// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:pigeon/src/ast.dart';
import 'package:pigeon/src/cpp/cpp_generator.dart';
import 'package:pigeon/src/generator_tools.dart';
import 'package:pigeon/src/pigeon_lib.dart' show Error;
import 'package:test/test.dart';

const String DEFAULT_PACKAGE_NAME = 'test_package';

final Class emptyClass = Class(name: 'className', fields: <NamedType>[
  NamedType(
    name: 'namedTypeName',
    type: const TypeDeclaration(baseName: 'baseName', isNullable: false),
  )
]);

final Enum emptyEnum = Enum(
  name: 'enumName',
  members: <EnumMember>[EnumMember(name: 'enumMemberName')],
);

void main() {
  test('gen one api', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          parameters: <Parameter>[
            Parameter(
                type: TypeDeclaration(
                  baseName: 'Input',
                  isNullable: false,
                  associatedClass: emptyClass,
                ),
                name: 'input')
          ],
          location: ApiLocation.host,
          returnType: TypeDeclaration(
            baseName: 'Output',
            isNullable: false,
            associatedClass: emptyClass,
          ),
        )
      ])
    ], classes: <Class>[
      Class(name: 'Input', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: true,
            ),
            name: 'input')
      ]),
      Class(name: 'Output', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: true,
            ),
            name: 'output')
      ])
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(generatorOptions, root, sink,
          dartPackageName: DEFAULT_PACKAGE_NAME);
      final String code = sink.toString();
      expect(code, contains('class Input'));
      expect(code, contains('class Output'));
      expect(code, contains('class Api'));
      expect(code, contains('virtual ~Api() {}\n'));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(generatorOptions, root, sink,
          dartPackageName: DEFAULT_PACKAGE_NAME);
      final String code = sink.toString();
      expect(code, contains('Input::Input()'));
      expect(code, contains('Output::Output'));
      expect(
          code,
          contains(RegExp(r'void Api::SetUp\(\s*'
              r'flutter::BinaryMessenger\* binary_messenger,\s*'
              r'Api\* api\s*\)')));
    }
  });

  test('naming follows style', () {
    final Enum anEnum = Enum(name: 'AnEnum', members: <EnumMember>[
      EnumMember(name: 'one'),
      EnumMember(name: 'fortyTwo'),
    ]);
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          parameters: <Parameter>[
            Parameter(
                type: TypeDeclaration(
                  baseName: 'Input',
                  isNullable: false,
                  associatedClass: emptyClass,
                ),
                name: 'someInput')
          ],
          location: ApiLocation.host,
          returnType: TypeDeclaration(
            baseName: 'Output',
            isNullable: false,
            associatedClass: emptyClass,
          ),
        )
      ])
    ], classes: <Class>[
      Class(name: 'Input', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'inputField')
      ]),
      Class(name: 'Output', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'outputField'),
        NamedType(
          type: TypeDeclaration(
            baseName: anEnum.name,
            isNullable: false,
            associatedEnum: anEnum,
          ),
          name: 'code',
        )
      ])
    ], enums: <Enum>[
      anEnum
    ]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(generatorOptions, root, sink,
          dartPackageName: DEFAULT_PACKAGE_NAME);
      final String code = sink.toString();
      // Method name and argument names should be adjusted.
      expect(code, contains(' DoSomething(const Input& some_input)'));
      // Getters and setters should use optional getter/setter style.
      expect(code, contains('bool input_field()'));
      expect(code, contains('void set_input_field(bool value_arg)'));
      expect(code, contains('bool output_field()'));
      expect(code, contains('void set_output_field(bool value_arg)'));
      // Instance variables should be adjusted.
      expect(code, contains('bool input_field_'));
      expect(code, contains('bool output_field_'));
      // Enum values should be adjusted.
      expect(code, contains('kOne'));
      expect(code, contains('kFortyTwo'));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(generatorOptions, root, sink,
          dartPackageName: DEFAULT_PACKAGE_NAME);
      final String code = sink.toString();
      expect(code, contains('encodable_some_input'));
      expect(code, contains('Output::output_field()'));
      expect(code, contains('Output::set_output_field(bool value_arg)'));
    }
  });

  test('FlutterError fields are private with public accessors', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: false,
                ),
                name: 'someInput')
          ],
          returnType: const TypeDeclaration(baseName: 'int', isNullable: false),
        )
      ])
    ], classes: <Class>[], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();

      expect(
          code.split('\n'),
          containsAllInOrder(<Matcher>[
            contains('class FlutterError {'),
            contains(' public:'),
            contains('  const std::string& code() const { return code_; }'),
            contains(
                '  const std::string& message() const { return message_; }'),
            contains(
                '  const flutter::EncodableValue& details() const { return details_; }'),
            contains(' private:'),
            contains('  std::string code_;'),
            contains('  std::string message_;'),
            contains('  flutter::EncodableValue details_;'),
          ]));
    }
  });

  test('Error field is private with public accessors', () {
    final Root root = Root(
      apis: <Api>[
        AstHostApi(name: 'Api', methods: <Method>[
          Method(
            name: 'doSomething',
            location: ApiLocation.host,
            parameters: <Parameter>[
              Parameter(
                  type: const TypeDeclaration(
                    baseName: 'int',
                    isNullable: false,
                  ),
                  name: 'someInput')
            ],
            returnType:
                const TypeDeclaration(baseName: 'int', isNullable: false),
          )
        ])
      ],
      classes: <Class>[],
      enums: <Enum>[],
      containsHostApi: true,
    );
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(generatorOptions, root, sink,
          dartPackageName: DEFAULT_PACKAGE_NAME);
      final String code = sink.toString();

      expect(
          code.split('\n'),
          containsAllInOrder(<Matcher>[
            contains('class ErrorOr {'),
            contains(' public:'),
            contains('  bool has_error() const {'),
            contains('  const T& value() const {'),
            contains('  const FlutterError& error() const {'),
            contains(' private:'),
            contains('  std::variant<T, FlutterError> v_;'),
          ]));
    }
  });

  test('Spaces before {', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                type: TypeDeclaration(
                  baseName: 'Input',
                  isNullable: false,
                  associatedClass: emptyClass,
                ),
                name: 'input')
          ],
          returnType: TypeDeclaration(
            baseName: 'Output',
            isNullable: false,
            associatedClass: emptyClass,
          ),
        )
      ])
    ], classes: <Class>[
      Class(name: 'Input', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: true,
            ),
            name: 'input')
      ]),
      Class(name: 'Output', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: true,
            ),
            name: 'output')
      ])
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(code, isNot(contains('){')));
      expect(code, isNot(contains('const{')));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(code, isNot(contains('){')));
      expect(code, isNot(contains('const{')));
    }
  });

  test('include blocks follow style', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: true,
                ),
                name: 'input')
          ],
          returnType: const TypeDeclaration(baseName: 'int', isNullable: false),
        )
      ])
    ], classes: <Class>[], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(code, contains('''
#include <flutter/basic_message_channel.h>
#include <flutter/binary_messenger.h>
#include <flutter/encodable_value.h>
#include <flutter/standard_message_codec.h>

#include <map>
#include <optional>
#include <string>
'''));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            headerIncludePath: 'a_header.h',
            cppHeaderOut: '',
            cppSourceOut: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(code, contains('''
#include "a_header.h"

#include <flutter/basic_message_channel.h>
#include <flutter/binary_messenger.h>
#include <flutter/encodable_value.h>
#include <flutter/standard_message_codec.h>

#include <map>
#include <optional>
#include <string>
'''));
    }
  });

  test('namespaces follows style', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: true,
                ),
                name: 'input')
          ],
          returnType: const TypeDeclaration(baseName: 'int', isNullable: false),
        )
      ])
    ], classes: <Class>[], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            namespace: 'foo',
            headerIncludePath: '',
            cppHeaderOut: '',
            cppSourceOut: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(code, contains('namespace foo {'));
      expect(code, contains('}  // namespace foo'));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            namespace: 'foo',
            headerIncludePath: '',
            cppHeaderOut: '',
            cppSourceOut: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(code, contains('namespace foo {'));
      expect(code, contains('}  // namespace foo'));
    }
  });

  test('data classes handle nullable fields', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                type: TypeDeclaration(
                  baseName: 'Input',
                  isNullable: false,
                  associatedClass: emptyClass,
                ),
                name: 'someInput')
          ],
          returnType: const TypeDeclaration.voidDeclaration(),
        )
      ])
    ], classes: <Class>[
      Class(name: 'Nested', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: true,
            ),
            name: 'nestedValue'),
      ]),
      Class(name: 'Input', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: true,
            ),
            name: 'nullableBool'),
        NamedType(
            type: const TypeDeclaration(
              baseName: 'int',
              isNullable: true,
            ),
            name: 'nullableInt'),
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: true,
            ),
            name: 'nullableString'),
        NamedType(
            type: TypeDeclaration(
              baseName: 'Nested',
              isNullable: true,
              associatedClass: emptyClass,
            ),
            name: 'nullableNested'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();

      // There should be a default constructor.
      expect(code, contains('Nested();'));
      // There should be a convenience constructor.
      expect(
          code,
          contains(
              RegExp(r'explicit Nested\(\s*const bool\* nested_value\s*\)')));

      // Getters should return const pointers.
      expect(code, contains('const bool* nullable_bool()'));
      expect(code, contains('const int64_t* nullable_int()'));
      expect(code, contains('const std::string* nullable_string()'));
      expect(code, contains('const Nested* nullable_nested()'));
      // Setters should take const pointers.
      expect(code, contains('void set_nullable_bool(const bool* value_arg)'));
      expect(code, contains('void set_nullable_int(const int64_t* value_arg)'));
      // Strings should be string_view rather than string as an argument.
      expect(
          code,
          contains(
              'void set_nullable_string(const std::string_view* value_arg)'));
      expect(
          code, contains('void set_nullable_nested(const Nested* value_arg)'));
      // Setters should have non-null-style variants.
      expect(code, contains('void set_nullable_bool(bool value_arg)'));
      expect(code, contains('void set_nullable_int(int64_t value_arg)'));
      expect(code,
          contains('void set_nullable_string(std::string_view value_arg)'));
      expect(
          code, contains('void set_nullable_nested(const Nested& value_arg)'));
      // Most instance variables should be std::optionals.
      expect(code, contains('std::optional<bool> nullable_bool_'));
      expect(code, contains('std::optional<int64_t> nullable_int_'));
      expect(code, contains('std::optional<std::string> nullable_string_'));
      // Custom classes are the exception, to avoid inline storage.
      expect(code, contains('std::unique_ptr<Nested> nullable_nested_'));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();

      // There should be a default constructor.
      expect(code, contains('Nested::Nested() {}'));
      // There should be a convenience constructor.
      expect(
          code,
          contains(RegExp(r'Nested::Nested\(\s*const bool\* nested_value\s*\)'
              r'\s*:\s*nested_value_\(nested_value \? '
              r'std::optional<bool>\(\*nested_value\) : std::nullopt\)\s*{}')));

      // Getters extract optionals.
      expect(code,
          contains('return nullable_bool_ ? &(*nullable_bool_) : nullptr;'));
      expect(code,
          contains('return nullable_int_ ? &(*nullable_int_) : nullptr;'));
      expect(
          code,
          contains(
              'return nullable_string_ ? &(*nullable_string_) : nullptr;'));
      expect(code, contains('return nullable_nested_.get();'));
      // Setters convert to optionals.
      expect(
          code,
          contains('nullable_bool_ = value_arg ? '
              'std::optional<bool>(*value_arg) : std::nullopt;'));
      expect(
          code,
          contains('nullable_int_ = value_arg ? '
              'std::optional<int64_t>(*value_arg) : std::nullopt;'));
      expect(
          code,
          contains('nullable_string_ = value_arg ? '
              'std::optional<std::string>(*value_arg) : std::nullopt;'));
      expect(
          code,
          contains(
              'nullable_nested_ = value_arg ? std::make_unique<Nested>(*value_arg) : nullptr;'));
      // Serialization handles optionals.
      expect(
          code,
          contains('nullable_bool_ ? EncodableValue(*nullable_bool_) '
              ': EncodableValue()'));
      expect(
          code,
          contains(
              'nullable_nested_ ? CustomEncodableValue(*nullable_nested_) : EncodableValue())'));

      // Serialization should use push_back, not initializer lists, to avoid
      // copies.
      expect(code, contains('list.reserve(4)'));
      expect(
          code,
          contains('list.push_back(nullable_bool_ ? '
              'EncodableValue(*nullable_bool_) : '
              'EncodableValue())'));
    }
  });

  test('data classes handle non-nullable fields', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                type: TypeDeclaration(
                  baseName: 'Input',
                  isNullable: false,
                  associatedClass: emptyClass,
                ),
                name: 'someInput')
          ],
          returnType: const TypeDeclaration.voidDeclaration(),
        )
      ])
    ], classes: <Class>[
      Class(name: 'Nested', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'nestedValue'),
      ]),
      Class(name: 'Input', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'nonNullableBool'),
        NamedType(
            type: const TypeDeclaration(
              baseName: 'int',
              isNullable: false,
            ),
            name: 'nonNullableInt'),
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: false,
            ),
            name: 'nonNullableString'),
        NamedType(
            type: TypeDeclaration(
              baseName: 'Nested',
              isNullable: false,
              associatedClass: emptyClass,
            ),
            name: 'nonNullableNested'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();

      // There should not be a default constructor.
      expect(code, isNot(contains('Nested();')));
      // There should be a convenience constructor.
      expect(code,
          contains(RegExp(r'explicit Nested\(\s*bool nested_value\s*\)')));

      // POD getters should return copies references.
      expect(code, contains('bool non_nullable_bool()'));
      expect(code, contains('int64_t non_nullable_int()'));
      // Non-POD getters should return const references.
      expect(code, contains('const std::string& non_nullable_string()'));
      expect(code, contains('const Nested& non_nullable_nested()'));
      // POD setters should take values.
      expect(code, contains('void set_non_nullable_bool(bool value_arg)'));
      expect(code, contains('void set_non_nullable_int(int64_t value_arg)'));
      // Strings should be string_view as an argument.
      expect(code,
          contains('void set_non_nullable_string(std::string_view value_arg)'));
      // Other non-POD setters should take const references.
      expect(code,
          contains('void set_non_nullable_nested(const Nested& value_arg)'));
      // Instance variables should be plain types.
      expect(code, contains('bool non_nullable_bool_;'));
      expect(code, contains('int64_t non_nullable_int_;'));
      expect(code, contains('std::string non_nullable_string_;'));
      // Except for custom classes.
      expect(code, contains('std::unique_ptr<Nested> non_nullable_nested_;'));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();

      // There should not be a default constructor.
      expect(code, isNot(contains('Nested::Nested() {}')));
      // There should be a convenience constructor.
      expect(
          code,
          contains(RegExp(r'Nested::Nested\(\s*bool nested_value\s*\)'
              r'\s*:\s*nested_value_\(nested_value\)\s*{}')));

      // Getters just return the value.
      expect(code, contains('return non_nullable_bool_;'));
      expect(code, contains('return non_nullable_int_;'));
      expect(code, contains('return non_nullable_string_;'));
      // Unless it's a custom class.
      expect(code, contains('return *non_nullable_nested_;'));
      // Setters just assign the value.
      expect(code, contains('non_nullable_bool_ = value_arg;'));
      expect(code, contains('non_nullable_int_ = value_arg;'));
      expect(code, contains('non_nullable_string_ = value_arg;'));
      // Unless it's a custom class.
      expect(
          code,
          contains(
              'non_nullable_nested_ = std::make_unique<Nested>(value_arg);'));
      // Serialization uses the value directly.
      expect(code, contains('EncodableValue(non_nullable_bool_)'));
      expect(code, contains('CustomEncodableValue(*non_nullable_nested_)'));

      // Serialization should use push_back, not initializer lists, to avoid
      // copies.
      expect(code, contains('list.reserve(4)'));
      expect(
          code,
          contains(
              'list.push_back(CustomEncodableValue(*non_nullable_nested_))'));
    }
  });

  test('host nullable return types map correctly', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'returnNullableBool',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'bool',
            isNullable: true,
          ),
        ),
        Method(
          name: 'returnNullableInt',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'int',
            isNullable: true,
          ),
        ),
        Method(
          name: 'returnNullableString',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'String',
            isNullable: true,
          ),
        ),
        Method(
          name: 'returnNullableList',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'List',
            typeArguments: <TypeDeclaration>[
              TypeDeclaration(
                baseName: 'String',
                isNullable: true,
              )
            ],
            isNullable: true,
          ),
        ),
        Method(
          name: 'returnNullableMap',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'Map',
            typeArguments: <TypeDeclaration>[
              TypeDeclaration(
                baseName: 'String',
                isNullable: true,
              ),
              TypeDeclaration(
                baseName: 'String',
                isNullable: true,
              )
            ],
            isNullable: true,
          ),
        ),
        Method(
          name: 'returnNullableDataClass',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: TypeDeclaration(
            baseName: 'ReturnData',
            isNullable: true,
            associatedClass: emptyClass,
          ),
        ),
      ])
    ], classes: <Class>[
      Class(name: 'ReturnData', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'aValue'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(
          code, contains('ErrorOr<std::optional<bool>> ReturnNullableBool()'));
      expect(code,
          contains('ErrorOr<std::optional<int64_t>> ReturnNullableInt()'));
      expect(
          code,
          contains(
              'ErrorOr<std::optional<std::string>> ReturnNullableString()'));
      expect(
          code,
          contains(
              'ErrorOr<std::optional<flutter::EncodableList>> ReturnNullableList()'));
      expect(
          code,
          contains(
              'ErrorOr<std::optional<flutter::EncodableMap>> ReturnNullableMap()'));
      expect(
          code,
          contains(
              'ErrorOr<std::optional<ReturnData>> ReturnNullableDataClass()'));
    }
  });

  test('host non-nullable return types map correctly', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'returnBool',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'bool',
            isNullable: false,
          ),
        ),
        Method(
          name: 'returnInt',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'int',
            isNullable: false,
          ),
        ),
        Method(
          name: 'returnString',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'String',
            isNullable: false,
          ),
        ),
        Method(
          name: 'returnList',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'List',
            typeArguments: <TypeDeclaration>[
              TypeDeclaration(
                baseName: 'String',
                isNullable: true,
              )
            ],
            isNullable: false,
          ),
        ),
        Method(
          name: 'returnMap',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration(
            baseName: 'Map',
            typeArguments: <TypeDeclaration>[
              TypeDeclaration(
                baseName: 'String',
                isNullable: true,
              ),
              TypeDeclaration(
                baseName: 'String',
                isNullable: true,
              )
            ],
            isNullable: false,
          ),
        ),
        Method(
          name: 'returnDataClass',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: TypeDeclaration(
            baseName: 'ReturnData',
            isNullable: false,
            associatedClass: emptyClass,
          ),
        ),
      ])
    ], classes: <Class>[
      Class(name: 'ReturnData', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'aValue'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(code, contains('ErrorOr<bool> ReturnBool()'));
      expect(code, contains('ErrorOr<int64_t> ReturnInt()'));
      expect(code, contains('ErrorOr<std::string> ReturnString()'));
      expect(code, contains('ErrorOr<flutter::EncodableList> ReturnList()'));
      expect(code, contains('ErrorOr<flutter::EncodableMap> ReturnMap()'));
      expect(code, contains('ErrorOr<ReturnData> ReturnDataClass()'));
    }
  });

  test('host nullable arguments map correctly', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                name: 'aBool',
                type: const TypeDeclaration(
                  baseName: 'bool',
                  isNullable: true,
                )),
            Parameter(
                name: 'anInt',
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: true,
                )),
            Parameter(
                name: 'aString',
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: true,
                )),
            Parameter(
                name: 'aList',
                type: const TypeDeclaration(
                  baseName: 'List',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'Object', isNullable: true)
                  ],
                  isNullable: true,
                )),
            Parameter(
                name: 'aMap',
                type: const TypeDeclaration(
                  baseName: 'Map',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'String', isNullable: true),
                    TypeDeclaration(baseName: 'Object', isNullable: true),
                  ],
                  isNullable: true,
                )),
            Parameter(
                name: 'anObject',
                type: TypeDeclaration(
                  baseName: 'ParameterObject',
                  isNullable: true,
                  associatedClass: emptyClass,
                )),
            Parameter(
                name: 'aGenericObject',
                type: const TypeDeclaration(
                  baseName: 'Object',
                  isNullable: true,
                )),
          ],
          returnType: const TypeDeclaration.voidDeclaration(),
        ),
      ])
    ], classes: <Class>[
      Class(name: 'ParameterObject', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'aValue'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(
          code,
          contains(RegExp(r'DoSomething\(\s*'
              r'const bool\* a_bool,\s*'
              r'const int64_t\* an_int,\s*'
              r'const std::string\* a_string,\s*'
              r'const flutter::EncodableList\* a_list,\s*'
              r'const flutter::EncodableMap\* a_map,\s*'
              r'const ParameterObject\* an_object,\s*'
              r'const flutter::EncodableValue\* a_generic_object\s*\)')));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      // Most types should just use get_if, since the parameter is a pointer,
      // and get_if will automatically handle null values (since a null
      // EncodableValue will not match the queried type, so get_if will return
      // nullptr).
      expect(
          code,
          contains(
              'const auto* a_bool_arg = std::get_if<bool>(&encodable_a_bool_arg);'));
      expect(
          code,
          contains(
              'const auto* a_string_arg = std::get_if<std::string>(&encodable_a_string_arg);'));
      expect(
          code,
          contains(
              'const auto* a_list_arg = std::get_if<EncodableList>(&encodable_a_list_arg);'));
      expect(
          code,
          contains(
              'const auto* a_map_arg = std::get_if<EncodableMap>(&encodable_a_map_arg);'));
      expect(
          code,
          contains(
              'const auto* a_bool_arg = std::get_if<bool>(&encodable_a_bool_arg);'));
      expect(
          code,
          contains(
              'const auto* an_int_arg = std::get_if<int64_t>(&encodable_an_int_arg);'));
      // Custom class types require an extra layer of extraction.
      expect(
          code,
          contains(
              'const auto* an_object_arg = encodable_an_object_arg.IsNull() ? nullptr : &(std::any_cast<const ParameterObject&>(std::get<CustomEncodableValue>(encodable_an_object_arg)));'));
      // "Object" requires no extraction at all since it has to use
      // EncodableValue directly.
      expect(
          code,
          contains(
              'const auto* a_generic_object_arg = &encodable_a_generic_object_arg;'));
    }
  });

  test('host non-nullable arguments map correctly', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                name: 'aBool',
                type: const TypeDeclaration(
                  baseName: 'bool',
                  isNullable: false,
                )),
            Parameter(
                name: 'anInt',
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: false,
                )),
            Parameter(
                name: 'aString',
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: false,
                )),
            Parameter(
                name: 'aList',
                type: const TypeDeclaration(
                  baseName: 'List',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'Object', isNullable: true)
                  ],
                  isNullable: false,
                )),
            Parameter(
                name: 'aMap',
                type: const TypeDeclaration(
                  baseName: 'Map',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'String', isNullable: true),
                    TypeDeclaration(baseName: 'Object', isNullable: true),
                  ],
                  isNullable: false,
                )),
            Parameter(
                name: 'anObject',
                type: TypeDeclaration(
                  baseName: 'ParameterObject',
                  isNullable: false,
                  associatedClass: emptyClass,
                )),
            Parameter(
                name: 'aGenericObject',
                type: const TypeDeclaration(
                  baseName: 'Object',
                  isNullable: false,
                )),
          ],
          returnType: const TypeDeclaration.voidDeclaration(),
        ),
      ])
    ], classes: <Class>[
      Class(name: 'ParameterObject', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'aValue'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(
          code,
          contains(RegExp(r'DoSomething\(\s*'
              r'bool a_bool,\s*'
              r'int64_t an_int,\s*'
              r'const std::string& a_string,\s*'
              r'const flutter::EncodableList& a_list,\s*'
              r'const flutter::EncodableMap& a_map,\s*'
              r'const ParameterObject& an_object,\s*'
              r'const flutter::EncodableValue& a_generic_object\s*\)')));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      // Most types should extract references. Since the type is non-nullable,
      // there's only one possible type.
      expect(
          code,
          contains(
              'const auto& a_bool_arg = std::get<bool>(encodable_a_bool_arg);'));
      expect(
          code,
          contains(
              'const auto& a_string_arg = std::get<std::string>(encodable_a_string_arg);'));
      expect(
          code,
          contains(
              'const auto& a_list_arg = std::get<EncodableList>(encodable_a_list_arg);'));
      expect(
          code,
          contains(
              'const auto& a_map_arg = std::get<EncodableMap>(encodable_a_map_arg);'));
      // Ints use a copy since there are two possible reference types, but
      // the parameter always needs an int64_t.
      expect(
          code,
          contains(
            'const int64_t an_int_arg = encodable_an_int_arg.LongValue();',
          ));
      // Custom class types require an extra layer of extraction.
      expect(
          code,
          contains(
              'const auto& an_object_arg = std::any_cast<const ParameterObject&>(std::get<CustomEncodableValue>(encodable_an_object_arg));'));
      // "Object" requires no extraction at all since it has to use
      // EncodableValue directly.
      expect(
          code,
          contains(
              'const auto& a_generic_object_arg = encodable_a_generic_object_arg;'));
    }
  });

  test('flutter nullable arguments map correctly', () {
    final Root root = Root(apis: <Api>[
      AstFlutterApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.flutter,
          parameters: <Parameter>[
            Parameter(
                name: 'aBool',
                type: const TypeDeclaration(
                  baseName: 'bool',
                  isNullable: true,
                )),
            Parameter(
                name: 'anInt',
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: true,
                )),
            Parameter(
                name: 'aString',
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: true,
                )),
            Parameter(
                name: 'aList',
                type: const TypeDeclaration(
                  baseName: 'List',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'Object', isNullable: true)
                  ],
                  isNullable: true,
                )),
            Parameter(
                name: 'aMap',
                type: const TypeDeclaration(
                  baseName: 'Map',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'String', isNullable: true),
                    TypeDeclaration(baseName: 'Object', isNullable: true),
                  ],
                  isNullable: true,
                )),
            Parameter(
                name: 'anObject',
                type: TypeDeclaration(
                  baseName: 'ParameterObject',
                  isNullable: true,
                  associatedClass: emptyClass,
                )),
            Parameter(
                name: 'aGenericObject',
                type: const TypeDeclaration(
                  baseName: 'Object',
                  isNullable: true,
                )),
          ],
          returnType: const TypeDeclaration(
            baseName: 'bool',
            isNullable: true,
          ),
        ),
      ])
    ], classes: <Class>[
      Class(name: 'ParameterObject', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'aValue'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      // Nullable arguments should all be pointers. This will make them somewhat
      // awkward for some uses (literals, values that could be inlined) but
      // unlike setters there's no way to provide reference-based alternatives
      // since it's not always just one argument.
      // TODO(stuartmorgan): Consider generating a second variant using
      // `std::optional`s; that may be more ergonomic, but the perf implications
      // would need to be considered.
      expect(
          code,
          contains(RegExp(r'DoSomething\(\s*'
              r'const bool\* a_bool,\s*'
              r'const int64_t\* an_int,\s*'
              // Nullable strings use std::string* rather than std::string_view*
              // since there's no implicit conversion for pointer types.
              r'const std::string\* a_string,\s*'
              r'const flutter::EncodableList\* a_list,\s*'
              r'const flutter::EncodableMap\* a_map,\s*'
              r'const ParameterObject\* an_object,\s*'
              r'const flutter::EncodableValue\* a_generic_object,')));
      // The callback should pass a pointer as well.
      expect(
          code,
          contains(
              RegExp(r'std::function<void\(const bool\*\)>&& on_success,\s*'
                  r'std::function<void\(const FlutterError&\)>&& on_error\)')));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      // All types pass nulls values when the pointer is null.
      // Standard types are wrapped an EncodableValues.
      expect(
          code,
          contains(
              'a_bool_arg ? EncodableValue(*a_bool_arg) : EncodableValue()'));
      expect(
          code,
          contains(
              'an_int_arg ? EncodableValue(*an_int_arg) : EncodableValue()'));
      expect(
          code,
          contains(
              'a_string_arg ? EncodableValue(*a_string_arg) : EncodableValue()'));
      expect(
          code,
          contains(
              'a_list_arg ? EncodableValue(*a_list_arg) : EncodableValue()'));
      expect(
          code,
          contains(
              'a_map_arg ? EncodableValue(*a_map_arg) : EncodableValue()'));
      // Class types use ToEncodableList.
      expect(
          code,
          contains(
              'an_object_arg ? CustomEncodableValue(*an_object_arg) : EncodableValue()'));
    }
  });

  test('flutter non-nullable arguments map correctly', () {
    final Root root = Root(apis: <Api>[
      AstFlutterApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.flutter,
          parameters: <Parameter>[
            Parameter(
                name: 'aBool',
                type: const TypeDeclaration(
                  baseName: 'bool',
                  isNullable: false,
                )),
            Parameter(
                name: 'anInt',
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: false,
                )),
            Parameter(
                name: 'aString',
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: false,
                )),
            Parameter(
                name: 'aList',
                type: const TypeDeclaration(
                  baseName: 'List',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'Object', isNullable: true)
                  ],
                  isNullable: false,
                )),
            Parameter(
                name: 'aMap',
                type: const TypeDeclaration(
                  baseName: 'Map',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'String', isNullable: true),
                    TypeDeclaration(baseName: 'Object', isNullable: true),
                  ],
                  isNullable: false,
                )),
            Parameter(
                name: 'anObject',
                type: TypeDeclaration(
                  baseName: 'ParameterObject',
                  isNullable: false,
                  associatedClass: emptyClass,
                )),
            Parameter(
                name: 'aGenericObject',
                type: const TypeDeclaration(
                  baseName: 'Object',
                  isNullable: false,
                )),
          ],
          returnType: const TypeDeclaration(
            baseName: 'bool',
            isNullable: false,
          ),
        ),
      ])
    ], classes: <Class>[
      Class(name: 'ParameterObject', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'aValue'),
      ]),
    ], enums: <Enum>[]);
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.header,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      expect(
          code,
          contains(RegExp(r'DoSomething\(\s*'
              r'bool a_bool,\s*'
              r'int64_t an_int,\s*'
              // Non-nullable strings use std::string for consistency with
              // nullable strings.
              r'const std::string& a_string,\s*'
              // Non-POD types use const references.
              r'const flutter::EncodableList& a_list,\s*'
              r'const flutter::EncodableMap& a_map,\s*'
              r'const ParameterObject& an_object,\s*'
              r'const flutter::EncodableValue& a_generic_object,\s*')));
      // The callback should pass a value.
      expect(
          code,
          contains(RegExp(r'std::function<void\(bool\)>&& on_success,\s*'
              r'std::function<void\(const FlutterError&\)>&& on_error\s*\)')));
    }
    {
      final StringBuffer sink = StringBuffer();
      const CppGenerator generator = CppGenerator();
      final OutputFileOptions<InternalCppOptions> generatorOptions =
          OutputFileOptions<InternalCppOptions>(
        fileType: FileType.source,
        languageOptions: const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
      );
      generator.generate(
        generatorOptions,
        root,
        sink,
        dartPackageName: DEFAULT_PACKAGE_NAME,
      );
      final String code = sink.toString();
      // Standard types are wrapped in EncodableValues.
      expect(code, contains('EncodableValue(a_bool_arg)'));
      expect(code, contains('EncodableValue(an_int_arg)'));
      expect(code, contains('EncodableValue(a_string_arg)'));
      expect(code, contains('EncodableValue(a_list_arg)'));
      expect(code, contains('EncodableValue(a_map_arg)'));
      // Class types are wrapped in CustomEncodableValues.
      expect(code, contains('CustomEncodableValue(an_object_arg)'));
    }
  });

  test('host API argument extraction uses references', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                name: 'anArg',
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: false,
                )),
          ],
          returnType: const TypeDeclaration.voidDeclaration(),
        ),
      ])
    ], classes: <Class>[], enums: <Enum>[]);

    final StringBuffer sink = StringBuffer();
    const CppGenerator generator = CppGenerator();
    final OutputFileOptions<InternalCppOptions> generatorOptions =
        OutputFileOptions<InternalCppOptions>(
      fileType: FileType.source,
      languageOptions: const InternalCppOptions(
          cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
    );
    generator.generate(
      generatorOptions,
      root,
      sink,
      dartPackageName: DEFAULT_PACKAGE_NAME,
    );
    final String code = sink.toString();
    // A bare 'auto' here would create a copy, not a reference, which is
    // inefficient.
    expect(
        code, contains('const auto& args = std::get<EncodableList>(message);'));
    expect(code, contains('const auto& encodable_an_arg_arg = args.at(0);'));
  });

  test('enum argument', () {
    final Root root = Root(
      apis: <Api>[
        AstHostApi(name: 'Bar', methods: <Method>[
          Method(
              name: 'bar',
              location: ApiLocation.host,
              returnType: const TypeDeclaration.voidDeclaration(),
              parameters: <Parameter>[
                Parameter(
                  name: 'foo',
                  type: TypeDeclaration(
                    baseName: 'Foo',
                    isNullable: false,
                    associatedEnum: emptyEnum,
                  ),
                )
              ])
        ])
      ],
      classes: <Class>[],
      enums: <Enum>[
        Enum(name: 'Foo', members: <EnumMember>[
          EnumMember(name: 'one'),
          EnumMember(name: 'two'),
        ])
      ],
    );
    final List<Error> errors = validateCpp(
        const InternalCppOptions(
            cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
        root);
    expect(errors.length, 1);
  });

  test('transfers documentation comments', () {
    final List<String> comments = <String>[
      ' api comment',
      ' api method comment',
      ' class comment',
      ' class field comment',
      ' enum comment',
      ' enum member comment',
    ];
    int count = 0;

    final List<String> unspacedComments = <String>['////////'];
    int unspacedCount = 0;

    final Root root = Root(
      apis: <Api>[
        AstFlutterApi(
          name: 'Api',
          documentationComments: <String>[comments[count++]],
          methods: <Method>[
            Method(
              name: 'method',
              location: ApiLocation.flutter,
              returnType: const TypeDeclaration.voidDeclaration(),
              documentationComments: <String>[comments[count++]],
              parameters: <Parameter>[
                Parameter(
                  name: 'field',
                  type: const TypeDeclaration(
                    baseName: 'int',
                    isNullable: true,
                  ),
                ),
              ],
            )
          ],
        )
      ],
      classes: <Class>[
        Class(
          name: 'class',
          documentationComments: <String>[comments[count++]],
          fields: <NamedType>[
            NamedType(
                documentationComments: <String>[comments[count++]],
                type: const TypeDeclaration(
                    baseName: 'Map',
                    isNullable: true,
                    typeArguments: <TypeDeclaration>[
                      TypeDeclaration(baseName: 'String', isNullable: true),
                      TypeDeclaration(baseName: 'int', isNullable: true),
                    ]),
                name: 'field1'),
          ],
        ),
      ],
      enums: <Enum>[
        Enum(
          name: 'enum',
          documentationComments: <String>[
            comments[count++],
            unspacedComments[unspacedCount++]
          ],
          members: <EnumMember>[
            EnumMember(
              name: 'one',
              documentationComments: <String>[comments[count++]],
            ),
            EnumMember(name: 'two'),
          ],
        ),
      ],
    );
    final StringBuffer sink = StringBuffer();
    const CppGenerator generator = CppGenerator();
    final OutputFileOptions<InternalCppOptions> generatorOptions =
        OutputFileOptions<InternalCppOptions>(
      fileType: FileType.header,
      languageOptions: const InternalCppOptions(
        headerIncludePath: 'foo',
        cppHeaderOut: '',
        cppSourceOut: '',
      ),
    );
    generator.generate(
      generatorOptions,
      root,
      sink,
      dartPackageName: DEFAULT_PACKAGE_NAME,
    );
    final String code = sink.toString();
    for (final String comment in comments) {
      expect(code, contains('//$comment'));
    }
    expect(code, contains('// ///'));
  });

  test('creates custom codecs', () {
    final Root root = Root(apis: <Api>[
      AstFlutterApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.flutter,
          parameters: <Parameter>[
            Parameter(
                type: TypeDeclaration(
                  baseName: 'Input',
                  isNullable: false,
                  associatedClass: emptyClass,
                ),
                name: '')
          ],
          returnType: TypeDeclaration(
            baseName: 'Output',
            isNullable: false,
            associatedClass: emptyClass,
          ),
          isAsynchronous: true,
        )
      ])
    ], classes: <Class>[
      Class(name: 'Input', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: true,
            ),
            name: 'input')
      ]),
      Class(name: 'Output', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'String',
              isNullable: true,
            ),
            name: 'output')
      ])
    ], enums: <Enum>[]);
    final StringBuffer sink = StringBuffer();
    const CppGenerator generator = CppGenerator();
    final OutputFileOptions<InternalCppOptions> generatorOptions =
        OutputFileOptions<InternalCppOptions>(
      fileType: FileType.header,
      languageOptions: const InternalCppOptions(
          cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
    );
    generator.generate(
      generatorOptions,
      root,
      sink,
      dartPackageName: DEFAULT_PACKAGE_NAME,
    );
    final String code = sink.toString();
    expect(code, contains(' : public flutter::StandardCodecSerializer'));
  });

  test('Does not send unwrapped EncodableLists', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'Api', methods: <Method>[
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                name: 'aBool',
                type: const TypeDeclaration(
                  baseName: 'bool',
                  isNullable: false,
                )),
            Parameter(
                name: 'anInt',
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: false,
                )),
            Parameter(
                name: 'aString',
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: false,
                )),
            Parameter(
                name: 'aList',
                type: const TypeDeclaration(
                  baseName: 'List',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'Object', isNullable: true)
                  ],
                  isNullable: false,
                )),
            Parameter(
                name: 'aMap',
                type: const TypeDeclaration(
                  baseName: 'Map',
                  typeArguments: <TypeDeclaration>[
                    TypeDeclaration(baseName: 'String', isNullable: true),
                    TypeDeclaration(baseName: 'Object', isNullable: true),
                  ],
                  isNullable: false,
                )),
            Parameter(
                name: 'anObject',
                type: TypeDeclaration(
                  baseName: 'ParameterObject',
                  isNullable: false,
                  associatedClass: emptyClass,
                )),
          ],
          returnType: const TypeDeclaration.voidDeclaration(),
        ),
      ])
    ], classes: <Class>[
      Class(name: 'ParameterObject', fields: <NamedType>[
        NamedType(
            type: const TypeDeclaration(
              baseName: 'bool',
              isNullable: false,
            ),
            name: 'aValue'),
      ]),
    ], enums: <Enum>[]);
    final StringBuffer sink = StringBuffer();
    const CppGenerator generator = CppGenerator();
    final OutputFileOptions<InternalCppOptions> generatorOptions =
        OutputFileOptions<InternalCppOptions>(
      fileType: FileType.source,
      languageOptions: const InternalCppOptions(
          cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
    );
    generator.generate(
      generatorOptions,
      root,
      sink,
      dartPackageName: DEFAULT_PACKAGE_NAME,
    );
    final String code = sink.toString();
    expect(code, isNot(contains('reply(wrap')));
    expect(code, contains('reply(EncodableValue('));
  });

  test('does not keep unowned references in async handlers', () {
    final Root root = Root(apis: <Api>[
      AstHostApi(name: 'HostApi', methods: <Method>[
        Method(
          name: 'noop',
          location: ApiLocation.host,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration.voidDeclaration(),
          isAsynchronous: true,
        ),
        Method(
          name: 'doSomething',
          location: ApiLocation.host,
          parameters: <Parameter>[
            Parameter(
                type: const TypeDeclaration(
                  baseName: 'int',
                  isNullable: false,
                ),
                name: '')
          ],
          returnType:
              const TypeDeclaration(baseName: 'double', isNullable: false),
          isAsynchronous: true,
        ),
      ]),
      AstFlutterApi(name: 'FlutterApi', methods: <Method>[
        Method(
          name: 'noop',
          location: ApiLocation.flutter,
          parameters: <Parameter>[],
          returnType: const TypeDeclaration.voidDeclaration(),
          isAsynchronous: true,
        ),
        Method(
          name: 'doSomething',
          location: ApiLocation.flutter,
          parameters: <Parameter>[
            Parameter(
                type: const TypeDeclaration(
                  baseName: 'String',
                  isNullable: false,
                ),
                name: '')
          ],
          returnType:
              const TypeDeclaration(baseName: 'bool', isNullable: false),
          isAsynchronous: true,
        ),
      ])
    ], classes: <Class>[], enums: <Enum>[]);
    final StringBuffer sink = StringBuffer();
    const CppGenerator generator = CppGenerator();
    final OutputFileOptions<InternalCppOptions> generatorOptions =
        OutputFileOptions<InternalCppOptions>(
      fileType: FileType.source,
      languageOptions: const InternalCppOptions(
          cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
    );
    generator.generate(
      generatorOptions,
      root,
      sink,
      dartPackageName: DEFAULT_PACKAGE_NAME,
    );
    final String code = sink.toString();
    // Nothing should be captured by reference for async handlers, since their
    // lifetime is unknown (and expected to be longer than the stack's).
    expect(code, isNot(contains('&reply')));
    expect(code, isNot(contains('&wrapped')));
    // Check for the exact capture format that is currently being used, to
    // ensure that the negative tests above get updated if there are any
    // changes to lambda capture.
    expect(code, contains('[reply]('));
  });

  test('connection error contains channel name', () {
    final Root root = Root(
      apis: <Api>[
        AstFlutterApi(
          name: 'Api',
          methods: <Method>[
            Method(
              name: 'method',
              location: ApiLocation.flutter,
              returnType: const TypeDeclaration.voidDeclaration(),
              parameters: <Parameter>[
                Parameter(
                  name: 'field',
                  type: const TypeDeclaration(
                    baseName: 'int',
                    isNullable: true,
                  ),
                ),
              ],
            )
          ],
        )
      ],
      classes: <Class>[],
      enums: <Enum>[],
      containsFlutterApi: true,
    );
    final StringBuffer sink = StringBuffer();
    const CppGenerator generator = CppGenerator();
    final OutputFileOptions<InternalCppOptions> generatorOptions =
        OutputFileOptions<InternalCppOptions>(
      fileType: FileType.source,
      languageOptions: const InternalCppOptions(
          cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
    );
    generator.generate(
      generatorOptions,
      root,
      sink,
      dartPackageName: DEFAULT_PACKAGE_NAME,
    );
    final String code = sink.toString();
    expect(
        code,
        contains(
            '"Unable to establish connection on channel: \'" + channel_name + "\'."'));
    expect(code, contains('on_error(CreateConnectionError(channel_name));'));
  });

  test('stack allocates the message channel.', () {
    final Root root = Root(
      apis: <Api>[
        AstFlutterApi(
          name: 'Api',
          methods: <Method>[
            Method(
              name: 'method',
              location: ApiLocation.flutter,
              returnType: const TypeDeclaration.voidDeclaration(),
              parameters: <Parameter>[
                Parameter(
                  name: 'field',
                  type: const TypeDeclaration(
                    baseName: 'int',
                    isNullable: true,
                  ),
                ),
              ],
            )
          ],
        )
      ],
      classes: <Class>[],
      enums: <Enum>[],
    );
    final StringBuffer sink = StringBuffer();
    const CppGenerator generator = CppGenerator();
    final OutputFileOptions<InternalCppOptions> generatorOptions =
        OutputFileOptions<InternalCppOptions>(
      fileType: FileType.source,
      languageOptions: const InternalCppOptions(
          cppHeaderOut: '', cppSourceOut: '', headerIncludePath: ''),
    );
    generator.generate(
      generatorOptions,
      root,
      sink,
      dartPackageName: DEFAULT_PACKAGE_NAME,
    );
    final String code = sink.toString();
    expect(
        code,
        contains(
            'BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec());'));
    expect(code, contains('channel.Send'));
  });
}