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

@TestOn('vm')
library;

import 'package:protoc_plugin/indenting_writer.dart';
import 'package:protoc_plugin/protoc.dart';
import 'package:protoc_plugin/src/gen/google/api/client.pb.dart';
import 'package:protoc_plugin/src/gen/google/protobuf/compiler/plugin.pb.dart';
import 'package:protoc_plugin/src/gen/google/protobuf/descriptor.pb.dart';
import 'package:protoc_plugin/src/linker.dart';
import 'package:protoc_plugin/src/options.dart';
import 'package:test/test.dart';

import 'src/golden_file.dart';
import 'src/test_features.dart';

FileDescriptorProto buildFileDescriptor({
  bool phoneNumber = true,
  bool topLevelEnum = false,
}) {
  final fd = FileDescriptorProto()..name = 'test';

  if (topLevelEnum) {
    fd.enumType.add(
      EnumDescriptorProto()
        ..name = 'PhoneType'
        ..value.addAll([
          EnumValueDescriptorProto()
            ..name = 'MOBILE'
            ..number = 0,
          EnumValueDescriptorProto()
            ..name = 'HOME'
            ..number = 1,
          EnumValueDescriptorProto()
            ..name = 'WORK'
            ..number = 2,
          EnumValueDescriptorProto()
            ..name = 'BUSINESS'
            ..number = 2,
        ]),
    );
  }

  if (phoneNumber) {
    fd.messageType.add(
      DescriptorProto()
        ..name = 'PhoneNumber'
        ..field.addAll([
          // required string number = 1;
          FieldDescriptorProto()
            ..name = 'number'
            ..jsonName = 'number'
            ..number = 1
            ..label = FieldDescriptorProto_Label.LABEL_REQUIRED
            ..type = FieldDescriptorProto_Type.TYPE_STRING,
          // optional int32 type = 2;
          // OR
          // optional PhoneType type = 2;
          FieldDescriptorProto()
            ..name = 'type'
            ..jsonName = 'type'
            ..number = 2
            ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
            ..type =
                topLevelEnum
                    ? FieldDescriptorProto_Type.TYPE_ENUM
                    : FieldDescriptorProto_Type.TYPE_INT32
            ..typeName = topLevelEnum ? '.PhoneType' : '',
          // optional string name = 3 [default = "$"];
          FieldDescriptorProto()
            ..name = 'name'
            ..jsonName = 'name'
            ..number = 3
            ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
            ..type = FieldDescriptorProto_Type.TYPE_STRING
            ..defaultValue = r'$',
        ]),
    );
  }
  return fd;
}

FileDescriptorProto createInt64Proto() {
  final fd = FileDescriptorProto()..name = 'test';
  fd.messageType.add(
    DescriptorProto()
      ..name = 'Int64'
      ..field.add(
        // optional int64 value = 1;
        FieldDescriptorProto()
          ..name = 'value'
          ..jsonName = 'value'
          ..number = 1
          ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
          ..type = FieldDescriptorProto_Type.TYPE_INT64,
      ),
  );

  return fd;
}

void main() {
  test(
    'FileGenerator outputs a .pb.dart file for a proto with one message',
    () {
      final fd = buildFileDescriptor();
      final options =
          parseGenerationOptions(
            CodeGeneratorRequest()..parameter = 'disable_constructor_args',
            CodeGeneratorResponse(),
          )!;
      final fg = FileGenerator(testEditionDefaults, fd, options);
      link(options, [fg]);
      expectGolden(
        fg.generateMainFile().emitSource(format: true),
        'oneMessage.pb.dart',
      );
    },
  );

  test('FileGenerator outputs a .pb.dart file for an Int64 message', () {
    final fd = createInt64Proto();
    final options =
        parseGenerationOptions(
          CodeGeneratorRequest()..parameter = 'disable_constructor_args',
          CodeGeneratorResponse(),
        )!;
    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);
    expectGolden(
      fg.generateMainFile().emitSource(format: true),
      'int64.pb.dart',
    );
  });

  test(
    'FileGenerator outputs a .pb.dart.meta file for a proto with one message',
    () {
      final fd = buildFileDescriptor();
      final options =
          parseGenerationOptions(
            CodeGeneratorRequest()
              ..parameter = 'generate_kythe_info,disable_constructor_args',
            CodeGeneratorResponse(),
          )!;
      final fg = FileGenerator(testEditionDefaults, fd, options);
      link(options, [fg]);
      expectGolden(
        fg.generateMainFile().sourceLocationInfo.toString(),
        'oneMessage.pb.dart.meta',
      );
    },
  );

  test(
    'FileGenerator outputs a pbjson.dart file for a proto with one message',
    () {
      final fd = buildFileDescriptor();
      final options =
          parseGenerationOptions(
            CodeGeneratorRequest()..parameter = 'disable_constructor_args',
            CodeGeneratorResponse(),
          )!;
      final fg = FileGenerator(testEditionDefaults, fd, options);
      link(options, [fg]);
      expectGolden(fg.generateJsonFile(), 'oneMessage.pbjson.dart');
    },
  );

  test('FileGenerator generates files for a top-level enum', () {
    final fd = buildFileDescriptor(phoneNumber: false, topLevelEnum: true);
    final options =
        parseGenerationOptions(
          CodeGeneratorRequest()..parameter = 'disable_constructor_args',
          CodeGeneratorResponse(),
        )!;

    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);
    expectGolden(
      fg.generateMainFile().emitSource(format: true),
      'topLevelEnum.pb.dart',
    );
    expectGolden(
      fg.generateEnumFile().emitSource(format: true),
      'topLevelEnum.pbenum.dart',
    );
  });

  test('FileGenerator generates metadata files for a top-level enum', () {
    final fd = buildFileDescriptor(phoneNumber: false, topLevelEnum: true);
    final options =
        parseGenerationOptions(
          CodeGeneratorRequest()
            ..parameter = 'generate_kythe_info,disable_constructor_args',
          CodeGeneratorResponse(),
        )!;
    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);

    expectGolden(
      fg.generateMainFile().sourceLocationInfo.toString(),
      'topLevelEnum.pb.dart.meta',
    );
    expectGolden(
      fg.generateEnumFile().sourceLocationInfo.toString(),
      'topLevelEnum.pbenum.dart.meta',
    );
  });

  test('FileGenerator generates a .pbjson.dart file for a top-level enum', () {
    final fd = buildFileDescriptor(phoneNumber: false, topLevelEnum: true);
    final options =
        parseGenerationOptions(
          CodeGeneratorRequest()..parameter = 'disable_constructor_args',
          CodeGeneratorResponse(),
        )!;

    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);
    expectGolden(fg.generateJsonFile(), 'topLevelEnum.pbjson.dart');
  });

  test('FileGenerator outputs library for a .proto in a package', () {
    final fd = buildFileDescriptor();
    fd.package = 'pb_library';
    final options =
        parseGenerationOptions(
          CodeGeneratorRequest()..parameter = 'disable_constructor_args',
          CodeGeneratorResponse(),
        )!;

    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);

    final writer = IndentingWriter();
    fg.writeMainHeader(writer);
    expectGolden(writer.emitSource(format: true), 'header_in_package.pb.dart');
  });

  test('FileGenerator outputs a fixnum import when needed', () {
    final fd =
        FileDescriptorProto()
          ..name = 'test'
          ..messageType.add(
            DescriptorProto()
              ..name = 'Count'
              ..field.addAll([
                FieldDescriptorProto()
                  ..name = 'count'
                  ..jsonName = 'count'
                  ..number = 1
                  ..type = FieldDescriptorProto_Type.TYPE_INT64,
              ]),
          );

    final options =
        parseGenerationOptions(
          CodeGeneratorRequest()..parameter = 'disable_constructor_args',
          CodeGeneratorResponse(),
        )!;

    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);

    final writer = IndentingWriter();
    fg.writeMainHeader(writer);
    expectGolden(writer.emitSource(format: true), 'header_with_fixnum.pb.dart');
  });

  test('FileGenerator outputs files for a service', () {
    final empty = DescriptorProto()..name = 'Empty';

    final sd =
        ServiceDescriptorProto()
          ..name = 'Test'
          ..method.add(
            MethodDescriptorProto()
              ..name = 'Ping'
              ..inputType = '.Empty'
              ..outputType = '.Empty',
          );

    final fd =
        FileDescriptorProto()
          ..name = 'test'
          ..messageType.add(empty)
          ..service.add(sd);

    final options =
        parseGenerationOptions(
          CodeGeneratorRequest()..parameter = 'disable_constructor_args',
          CodeGeneratorResponse(),
        )!;

    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);

    final writer = IndentingWriter();
    fg.writeMainHeader(writer);
    expectGolden(
      fg.generateMainFile().emitSource(format: true),
      'service.pb.dart',
    );
    expectGolden(fg.generateServerFile(), 'service.pbserver.dart');
  });

  test(
    'FileGenerator does not output legacy service stubs if gRPC is selected',
    () {
      final empty = DescriptorProto()..name = 'Empty';

      final sd =
          ServiceDescriptorProto()
            ..name = 'Test'
            ..method.add(
              MethodDescriptorProto()
                ..name = 'Ping'
                ..inputType = '.Empty'
                ..outputType = '.Empty',
            );

      final fd =
          FileDescriptorProto()
            ..name = 'test'
            ..messageType.add(empty)
            ..service.add(sd);

      final options = GenerationOptions(useGrpc: true);

      final fg = FileGenerator(testEditionDefaults, fd, options);
      link(options, [fg]);

      final writer = IndentingWriter();
      fg.writeMainHeader(writer);
      expectGolden(
        fg.generateMainFile().emitSource(format: true),
        'grpc_service.pb.dart',
      );
    },
  );

  test('FileGenerator outputs gRPC stubs if gRPC is selected', () {
    final input = DescriptorProto()..name = 'Input';
    final output = DescriptorProto()..name = 'Output';

    final unary =
        MethodDescriptorProto()
          ..name = 'Unary'
          ..inputType = '.Input'
          ..outputType = '.Output'
          ..clientStreaming = false
          ..serverStreaming = false;

    final clientStreaming =
        MethodDescriptorProto()
          ..name = 'ClientStreaming'
          ..inputType = '.Input'
          ..outputType = '.Output'
          ..clientStreaming = true
          ..serverStreaming = false;

    final serverStreaming =
        MethodDescriptorProto()
          ..name = 'ServerStreaming'
          ..inputType = '.Input'
          ..outputType = '.Output'
          ..clientStreaming = false
          ..serverStreaming = true;

    final bidirectional =
        MethodDescriptorProto()
          ..name = 'Bidirectional'
          ..inputType = '.Input'
          ..outputType = '.Output'
          ..clientStreaming = true
          ..serverStreaming = true;

    // A method with name 'call' to test that it doesn't conflict with the
    // method arguments with the same name, see issue #963.
    final keywordCall =
        MethodDescriptorProto()
          ..name = 'Call'
          ..inputType = '.Input'
          ..outputType = '.Output'
          ..clientStreaming = false
          ..serverStreaming = false;

    // A method with name 'request' to test that it doesn't conflict with the
    // method arguments with the same name, see issue #159.
    final keywordRequest =
        MethodDescriptorProto()
          ..name = 'Request'
          ..inputType = '.Input'
          ..outputType = '.Output'
          ..clientStreaming = false
          ..serverStreaming = false;

    final serviceOptions = ServiceOptions();
    serviceOptions.setExtension(Client.defaultHost, 'www.example.com');
    serviceOptions.setExtension(
      Client.oauthScopes,
      'https://www.googleapis.com/auth/cloud-platform,'
      'https://www.googleapis.com/auth/datastore',
    );

    final sd =
        ServiceDescriptorProto()
          ..name = 'Test'
          ..options = serviceOptions
          ..method.addAll([
            unary,
            clientStreaming,
            serverStreaming,
            bidirectional,
            keywordCall,
            keywordRequest,
          ]);

    final fd =
        FileDescriptorProto()
          ..name = 'test'
          ..messageType.addAll([input, output])
          ..service.add(sd);

    final options = GenerationOptions(useGrpc: true);

    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [fg]);

    final writer = IndentingWriter();
    fg.writeMainHeader(writer);
    // We use a '.~dart' file extension here, insead of '.dart', so that
    // 'pub publish' won't try and validate that all the imports for this file
    // are listed in the pubspec.
    expectGolden(fg.generateGrpcFile(), 'grpc_service.pbgrpc.~dart');
  });

  test('FileGenerator generates imports for .pb.dart files', () {
    // This defines three .proto files package1.proto, package2.proto and
    // test.proto with the following content:
    //
    // package1.proto:
    // ---------------
    // package p1;
    // message M {
    //   optional M m = 2;
    // }
    //
    // package2.proto:
    // ---------------
    // package p2;
    // message M {
    //   optional M m = 2;
    // }
    //
    // test.proto:
    // ---------------
    // package test;
    // import "package1.proto";
    // import "package2.proto";
    // message M {
    //   optional M m = 1;
    //   optional p1.M m1 = 2;
    //   optional p2.M m2 = 3;
    // }

    // Description of package1.proto.
    final md1 =
        DescriptorProto()
          ..name = 'M'
          ..field.addAll([
            // optional M m = 1;
            FieldDescriptorProto()
              ..name = 'm'
              ..jsonName = 'm'
              ..number = 1
              ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
              ..type = FieldDescriptorProto_Type.TYPE_MESSAGE
              ..typeName = '.p1.M',
          ]);
    final fd1 =
        FileDescriptorProto()
          ..package = 'p1'
          ..name = 'package1.proto'
          ..messageType.add(md1);

    // Description of package1.proto.
    final md2 =
        DescriptorProto()
          ..name = 'M'
          ..field.addAll([
            // optional M m = 1;
            FieldDescriptorProto()
              ..name = 'x'
              ..jsonName = 'x'
              ..number = 1
              ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
              ..type = FieldDescriptorProto_Type.TYPE_MESSAGE
              ..typeName = '.p2.M',
          ]);
    final fd2 =
        FileDescriptorProto()
          ..package = 'p2'
          ..name = 'package2.proto'
          ..messageType.add(md2);

    // Description of test.proto.
    final md =
        DescriptorProto()
          ..name = 'M'
          ..field.addAll([
            // optional M m = 1;
            FieldDescriptorProto()
              ..name = 'm'
              ..jsonName = 'm'
              ..number = 1
              ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
              ..type = FieldDescriptorProto_Type.TYPE_MESSAGE
              ..typeName = '.M',
            // optional p1.M m1 = 2;
            FieldDescriptorProto()
              ..name = 'm1'
              ..jsonName = 'm1'
              ..number = 2
              ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
              ..type = FieldDescriptorProto_Type.TYPE_MESSAGE
              ..typeName = '.p1.M',
            // optional p2.M m2 = 3;
            FieldDescriptorProto()
              ..name = 'm2'
              ..jsonName = 'm2'
              ..number = 3
              ..label = FieldDescriptorProto_Label.LABEL_OPTIONAL
              ..type = FieldDescriptorProto_Type.TYPE_MESSAGE
              ..typeName = '.p2.M',
          ]);
    final fd =
        FileDescriptorProto()
          ..name = 'test.proto'
          ..messageType.add(md);
    fd.dependency.addAll(['package1.proto', 'package2.proto']);
    final request =
        CodeGeneratorRequest()..parameter = 'disable_constructor_args';
    final response = CodeGeneratorResponse();
    final options = parseGenerationOptions(request, response)!;

    final fg = FileGenerator(testEditionDefaults, fd, options);
    link(options, [
      fg,
      FileGenerator(testEditionDefaults, fd1, options),
      FileGenerator(testEditionDefaults, fd2, options),
    ]);
    expectGolden(
      fg.generateMainFile().emitSource(format: true),
      'imports.pb.dart',
    );
    expectGolden(
      fg.generateEnumFile().emitSource(format: true),
      'imports.pbjson.dart',
    );
  });

  test('FileGenerator rejects files without valid edition defaults', () {
    final fd = buildFileDescriptor();
    final editionDefaults =
        FeatureSetDefaults()
          ..defaults.add(
            FeatureSetDefaults_FeatureSetEditionDefault()
              ..edition = Edition.EDITION_2023
              ..overridableFeatures =
                  testEditionDefaults.defaults[0].overridableFeatures,
          )
          ..minimumEdition = Edition.EDITION_PROTO2
          ..maximumEdition = Edition.EDITION_2023;

    expect(
      () => FileGenerator(editionDefaults, fd, GenerationOptions()),
      throwsA(
        const TypeMatcher<ArgumentError>().having(
          (e) => e.message,
          'message',
          allOf(contains('No default found'), contains('EDITION_PROTO2')),
        ),
      ),
    );
  });

  test('FileGenerator rejects files before the minimum supported edition', () {
    final fd = buildFileDescriptor();
    final editionDefaults =
        FeatureSetDefaults()
          ..defaults.addAll(testEditionDefaults.defaults.sublist(1))
          ..minimumEdition = Edition.EDITION_PROTO3
          ..maximumEdition = Edition.EDITION_2023;

    expect(
      () => FileGenerator(editionDefaults, fd, GenerationOptions()),
      throwsA(
        const TypeMatcher<ArgumentError>().having(
          (e) => e.message,
          'message',
          contains('earlier than the minimum'),
        ),
      ),
    );
  });

  test('FileGenerator rejects files after the maximum supported edition', () {
    final fd = buildFileDescriptor()..edition = Edition.EDITION_2023;
    final editionDefaults =
        FeatureSetDefaults()
          ..defaults.addAll(testEditionDefaults.defaults)
          ..minimumEdition = Edition.EDITION_PROTO2
          ..maximumEdition = Edition.EDITION_PROTO3;

    expect(
      () => FileGenerator(editionDefaults, fd, GenerationOptions()),
      throwsA(
        const TypeMatcher<ArgumentError>().having(
          (e) => e.message,
          'message',
          contains('later than the maximum'),
        ),
      ),
    );
  });

  test('FileGenerator initializes the file-level edition defaults', () {
    final fd = buildFileDescriptor();
    final editionDefaults = testEditionDefaults.deepCopy();
    setTestFeature(editionDefaults.defaults[0].overridableFeatures, 1);

    final fg = FileGenerator(editionDefaults, fd, GenerationOptions());
    expect(fg.features.enumType, FeatureSet_EnumType.CLOSED);
    expect(fg.features.fieldPresence, FeatureSet_FieldPresence.EXPLICIT);
    expect(
      fg.features.messageEncoding,
      FeatureSet_MessageEncoding.LENGTH_PREFIXED,
    );
    expect(fg.features.utf8Validation, FeatureSet_Utf8Validation.NONE);
    expect(
      fg.features.repeatedFieldEncoding,
      FeatureSet_RepeatedFieldEncoding.EXPANDED,
    );
    expect(fg.features.jsonFormat, FeatureSet_JsonFormat.LEGACY_BEST_EFFORT);
    expect(getTestFeature(fg.features), 1);
  });

  test('FileGenerator uses file-level overrides', () {
    final fd = setTestFeature(
      buildFileDescriptor()..edition = Edition.EDITION_2023,
      2,
    );
    final editionDefaults = testEditionDefaults.deepCopy();
    setTestFeature(editionDefaults.defaults[0].overridableFeatures, 1);

    final fg = FileGenerator(editionDefaults, fd, GenerationOptions());
    expect(fg.features.enumType, FeatureSet_EnumType.OPEN);
    expect(fg.features.fieldPresence, FeatureSet_FieldPresence.EXPLICIT);
    expect(
      fg.features.messageEncoding,
      FeatureSet_MessageEncoding.LENGTH_PREFIXED,
    );
    expect(fg.features.utf8Validation, FeatureSet_Utf8Validation.VERIFY);
    expect(
      fg.features.repeatedFieldEncoding,
      FeatureSet_RepeatedFieldEncoding.PACKED,
    );
    expect(fg.features.jsonFormat, FeatureSet_JsonFormat.ALLOW);
    expect(getTestFeature(fg.features), 2);
  });
}
