// Copyright (c) 2015, 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.

import 'package:protobuf/protobuf.dart';
import 'package:test/test.dart';

import 'gen/ExtensionEnumNameConflict.pb.dart';
import 'gen/ExtensionNameConflict.pb.dart';
import 'gen/enum_extension.pb.dart';
import 'gen/extend_unittest.pb.dart';
import 'gen/google/protobuf/unittest.pb.dart';
import 'gen/nested_extension.pb.dart';
import 'gen/non_nested_extension.pb.dart';
import 'src/test_util.dart';

Matcher throwsArgError(String expectedMessage) => throwsA(
  predicate((dynamic x) {
    expect(x, isArgumentError);
    expect(x.message, expectedMessage);
    return true;
  }),
);

final withExtensions =
    TestAllExtensions()
      ..setExtension(
        Unittest.optionalForeignMessageExtension,
        ForeignMessage()..c = 3,
      )
      ..setExtension(Unittest.defaultStringExtension, 'bar')
      ..getExtension(Unittest.repeatedBytesExtension).add('pop'.codeUnits)
      ..setExtension(
        Extend_unittest.outer,
        Outer()
          ..inner = (Inner()..value = 'abc')
          ..setExtension(
            Extend_unittest.extensionInner,
            Inner()..value = 'def',
          ),
      );

void main() {
  test('can set all extension types', () {
    final message = TestAllExtensions();
    setAllExtensions(message);
    assertAllExtensionsSet(message);
  });

  test('can modify all repeated extension types', () {
    final message = TestAllExtensions();
    setAllExtensions(message);
    modifyRepeatedExtensions(message);
    assertRepeatedExtensionsModified(message);
  });

  test('unset extensions return default values', () {
    assertExtensionsClear(TestAllExtensions());
  });

  // void testExtensionReflectionGetters() {} // UNSUPPORTED -- reflection
  // void testExtensionReflectionSetters() {} // UNSUPPORTED -- reflection
  // void testExtensionReflectionSettersRejectNull() {} // UNSUPPORTED
  // void testExtensionReflectionRepeatedSetters() {} // UNSUPPORTED
  // void testExtensionReflectionRepeatedSettersRejectNull() // UNSUPPORTED
  // void testExtensionReflectionDefaults() // UNSUPPORTED

  test('can clear an optional extension', () {
    // clearExtension() is not actually used in test_util, so try it manually.
    final message = TestAllExtensions();
    message.setExtension(Unittest.optionalInt32Extension, 1);
    message.clearExtension(Unittest.optionalInt32Extension);
    expect(message.hasExtension(Unittest.optionalInt32Extension), isFalse);
  });

  test(
    'clearing extension throws when the message is frozen, without extension field set',
    () {
      final message = TestAllExtensions()..freeze();
      expect(
        () => message.clearExtension(Unittest.optionalInt32Extension),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    },
  );

  test(
    'clearing extension throws when the message is frozen, with extension field set',
    () {
      final message =
          TestAllExtensions()
            ..setExtension(Unittest.optionalInt32Extension, 1)
            ..freeze();
      expect(
        () => message.clearExtension(Unittest.optionalInt32Extension),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    },
  );

  test('can clear a repeated extension', () {
    final message = TestAllExtensions();
    message.addExtension(Unittest.repeatedInt32Extension, 1);
    message.clearExtension(Unittest.repeatedInt32Extension);
    expect(message.getExtension(Unittest.repeatedInt32Extension).length, 0);
  });

  test('can clone an extension field', () {
    final original = TestAllExtensions();
    original.setExtension(Unittest.optionalInt32Extension, 1);
    final clone = original.deepCopy();
    expect(clone.hasExtension(Unittest.optionalInt32Extension), isTrue);
    expect(clone.getExtension(Unittest.optionalInt32Extension), 1);
  });

  test('can clone all types of extension fields', () {
    assertAllExtensionsSet(getAllExtensionsSet().deepCopy());
  });

  test('can merge extension', () {
    final mergeSource =
        TestAllExtensions()..addExtension(Unittest.repeatedInt32Extension, 123);

    final mergeDest =
        TestAllExtensions()..addExtension(Unittest.repeatedInt32Extension, 456);

    final result =
        TestAllExtensions()
          ..mergeFromMessage(mergeSource)
          ..mergeFromMessage(mergeDest);

    expect(result.getExtension(Unittest.repeatedInt32Extension), [123, 456]);
  });

  test("throws if field number isn't allowed for extension", () {
    final message = TestAllTypes(); // does not allow extensions
    expect(
      () {
        message.setExtension(Unittest.optionalInt32Extension, 0);
      },
      throwsArgError(
        'Extension optionalInt32Extension not legal for message proto2_unittest.TestAllTypes',
      ),
    );

    expect(
      () {
        message.getExtension(Unittest.optionalInt32Extension);
      },
      throwsArgError(
        'Extension optionalInt32Extension not legal for message proto2_unittest.TestAllTypes',
      ),
    );
  });

  test('throws if an int32 extension is set to a bad value', () {
    final message = TestAllExtensions();
    expect(
      () {
        message.setExtension(Unittest.optionalInt32Extension, 'hello');
      },
      throwsArgError(
        'Illegal to set field optionalInt32Extension (1) of proto2_unittest.TestAllExtensions'
        ' to value (hello): not type int',
      ),
    );
  });

  test('throws if an int64 extension is set to a bad value', () {
    final message = TestAllExtensions();
    expect(
      () {
        message.setExtension(Unittest.optionalInt64Extension, 123);
      },
      throwsArgError(
        'Illegal to set field optionalInt64Extension (2) of proto2_unittest.TestAllExtensions'
        ' to value (123): not Int64',
      ),
    );
  });

  test('throws if a message extension is set to a bad value', () {
    final message = TestAllExtensions();

    // For a non-repeated message, we only check for a GeneratedMessage.
    expect(
      () {
        message.setExtension(Unittest.optionalNestedMessageExtension, 123);
      },
      throwsArgError(
        'Illegal to set field optionalNestedMessageExtension (18)'
        ' of proto2_unittest.TestAllExtensions to value (123): not a GeneratedMessage',
      ),
    );

    // For a repeated message, the type check is exact.
    expect(() {
      message.addExtension(
        Unittest.repeatedNestedMessageExtension,
        TestAllTypes(),
      );
    }, throwsATypeError);
  });

  test('throws if an enum extension is set to a bad value', () {
    final message = TestAllExtensions();

    // For a non-repeated enum, we only check for a ProtobufEnum.
    expect(
      () {
        message.setExtension(Unittest.optionalNestedEnumExtension, 123);
      },
      throwsArgError(
        'Illegal to set field optionalNestedEnumExtension (21)'
        ' of proto2_unittest.TestAllExtensions to value (123): not type ProtobufEnum',
      ),
    );

    // For a repeated enum, the type check is exact.
    expect(() {
      message.addExtension(
        Unittest.repeatedForeignEnumExtension,
        TestAllTypes_NestedEnum.FOO,
      );
    }, throwsATypeError);
  });

  test('can extend a message with a message field with a different type', () {
    expect(
      Non_nested_extension.nonNestedExtension.makeDefault!(),
      TypeMatcher<MyNonNestedExtension>(),
    );
    expect(Non_nested_extension.nonNestedExtension.name, 'nonNestedExtension');
  });

  test('can extend a message with a message field of the same type', () {
    expect(
      MyNestedExtension.recursiveExtension.makeDefault!()
          is MessageToBeExtended,
      isTrue,
    );
    expect(MyNestedExtension.recursiveExtension.name, 'recursiveExtension');
  });

  test('can extend message with enum', () {
    final msg = Extendable();
    msg.setExtension(Enum_extension.animal, Animal.CAT);
  });

  test('extension class was renamed to avoid conflict with message', () {
    expect(ExtensionNameConflictExt.someExtension.tagNumber, 1);
  });

  test('extension class was renamed to avoid conflict with enum', () {
    expect(ExtensionEnumNameConflictExt.enumConflictExtension.tagNumber, 1);
  });

  test('to toDebugString', () {
    final value =
        TestAllExtensions()
          ..setExtension(Unittest.optionalInt32Extension, 1)
          ..addExtension(Unittest.repeatedStringExtension, 'hello')
          ..addExtension(Unittest.repeatedStringExtension, 'world')
          ..setExtension(
            Unittest.optionalNestedMessageExtension,
            TestAllTypes_NestedMessage()..bb = 42,
          )
          ..setExtension(
            Unittest.optionalNestedEnumExtension,
            TestAllTypes_NestedEnum.BAR,
          );

    final expected =
        '[optionalInt32Extension]: 1\n'
        '[optionalNestedMessageExtension]: {\n'
        '  bb: 42\n'
        '}\n'
        '[optionalNestedEnumExtension]: BAR\n'
        '[repeatedStringExtension]: hello\n'
        '[repeatedStringExtension]: world\n';

    expect(value.toString(), expected);
  });

  test('can compare messages with and without extensions', () {
    final withExtension =
        TestFieldOrderings()
          ..myString = 'foo'
          ..setExtension(Unittest.myExtensionString, 'bar');
    final b = withExtension.writeToBuffer();
    final withUnknownField = TestFieldOrderings.fromBuffer(b);
    final r = ExtensionRegistry();
    Unittest.registerAllExtensions(r);
    final decodedWithExtension = TestFieldOrderings.fromBuffer(b, r);
    final noExtension = TestFieldOrderings()..myString = 'foo';
    expect(withExtension == decodedWithExtension, true);
    expect(withUnknownField == decodedWithExtension, false);
    expect(decodedWithExtension == withUnknownField, false);
    expect(withUnknownField == noExtension, false);
    expect(noExtension == withUnknownField, false);
    decodedWithExtension.setExtension(Unittest.myExtensionInt, 42);
    expect(withExtension == decodedWithExtension, false);
    expect(decodedWithExtension == withExtension, false);
    expect(decodedWithExtension == withExtension, false);
  });

  test(
    'ExtensionRegistry.reparseMessage will preserve already registered extensions',
    () {
      final r1 = ExtensionRegistry();
      Unittest.registerAllExtensions(r1);

      final r2 = ExtensionRegistry();
      Extend_unittest.registerAllExtensions(r2);
      final withUnknownFields = TestAllExtensions.fromBuffer(
        withExtensions.writeToBuffer(),
      );
      final reparsedR1 = r1.reparseMessage(withUnknownFields);

      expect(
        reparsedR1.getExtension(Unittest.optionalForeignMessageExtension).c,
        3,
      );

      expect(
        r2
            .reparseMessage(reparsedR1)
            .getExtension(Unittest.optionalForeignMessageExtension)
            .c,
        3,
      );
    },
  );

  test(
    'ExtensionRegistry.reparseMessage reparses extensions that were not in the original registry',
    () {
      final r = ExtensionRegistry();
      Unittest.registerAllExtensions(r);
      Extend_unittest.registerAllExtensions(r);

      final withUnknownFields = TestAllExtensions.fromBuffer(
        withExtensions.writeToBuffer(),
      );
      final reparsed = r.reparseMessage(withUnknownFields);

      expect(
        () => withUnknownFields.getExtension(Unittest.defaultStringExtension),
        throwsA(const TypeMatcher<StateError>()),
      );

      expect(reparsed.getExtension(Unittest.defaultStringExtension), 'bar');

      expect(
        reparsed.unknownFields.getField(
          Unittest.defaultStringExtension.tagNumber,
        ),
        null,
        reason:
            'ExtensionRegistry.reparseMessage does not leave reparsed fields in unknownFields',
      );
      expect(reparsed.getExtension(Unittest.repeatedBytesExtension), [
        'pop'.codeUnits,
      ]);

      expect(
        reparsed.getExtension(Unittest.optionalForeignMessageExtension).c,
        3,
      );

      expect(reparsed.getExtension(Extend_unittest.outer).inner.value, 'abc');

      final onlyOuter = ExtensionRegistry()..add(Extend_unittest.outer);
      final onlyOuterReparsed = onlyOuter.reparseMessage(withUnknownFields);
      expect(
        onlyOuterReparsed
            .getExtension(Extend_unittest.outer)
            .hasExtension(Extend_unittest.extensionInner),
        isFalse,
      );
      expect(
        () => onlyOuter
            .reparseMessage(withUnknownFields)
            .getExtension(Extend_unittest.outer)
            .getExtension(Extend_unittest.extensionInner),
        throwsA(const TypeMatcher<StateError>()),
      );

      expect(
        reparsed
            .getExtension(Extend_unittest.outer)
            .hasExtension(Extend_unittest.extensionInner),
        isTrue,
      );
      expect(
        reparsed
            .getExtension(Extend_unittest.outer)
            .getExtension(Extend_unittest.extensionInner)
            .value,
        'def',
      );
    },
  );

  test('ExtensionRegistry.reparseMessage does not update the original', () {
    final r = ExtensionRegistry();
    Unittest.registerAllExtensions(r);
    Extend_unittest.registerAllExtensions(r);

    final withUnknownFields = TestAllExtensions.fromBuffer(
      withExtensions.writeToBuffer(),
    );

    final reparsedWithEmpty = ExtensionRegistry().reparseMessage(
      withUnknownFields,
    );
    expect(reparsedWithEmpty, same(withUnknownFields));

    final reparsed = r.reparseMessage(withUnknownFields);

    final strings =
        withUnknownFields.getExtension(Unittest.repeatedStringExtension)
            as List<String>;
    expect(strings, []);
    strings.add('pop2');
    expect(reparsed.getExtension(Unittest.repeatedStringExtension), []);
  });

  test('ExtensionRegistry.reparseMessage will throw on malformed buffers', () {
    final r = ExtensionRegistry();
    Unittest.registerAllExtensions(r);

    final r2 = ExtensionRegistry();
    Extend_unittest.registerAllExtensions(r2);

    // The message encoded in this buffer has an encoding error in the
    // Extend_unittest.outer extension field.
    final withMalformedExtensionEncoding = TestAllExtensions.fromBuffer([
      154, 1, 2, 8, 3, 210, //
      4, 3, 98, 97, 114, 194, 6, 14, 10, 5, 10, 4,
      97, 98, 99, 18, 5, 10, 3, 100, 101, 102,
    ]);
    expect(
      r
          .reparseMessage(withMalformedExtensionEncoding)
          .getExtension(Unittest.defaultStringExtension),
      'bar',
      reason: 'this succeeds because it does not decode Extend_unittest.outer',
    );
    expect(
      () => r2.reparseMessage(withMalformedExtensionEncoding),
      throwsA(const TypeMatcher<InvalidProtocolBufferException>()),
    );
  });

  test('ExtensionRegistry.reparseMessage preserves frozenness', () {
    final r = ExtensionRegistry();
    Unittest.registerAllExtensions(r);

    final withUnknownFields = TestAllExtensions.fromBuffer(
      withExtensions.writeToBuffer(),
    );
    withUnknownFields.freeze();

    expect(
      () => r
          .reparseMessage(withUnknownFields)
          .setExtension(Unittest.defaultStringExtension, 'blah'),
      throwsA(TypeMatcher<UnsupportedError>()),
    );
  });

  test(
    'ExtensionRegistry.reparseMessage returns the same object when no new extensions are parsed',
    () {
      final original =
          Outer()
            ..inner =
                (Inner()
                  ..innerMost =
                      (InnerMost()..setExtension(
                        Extend_unittest.innerMostExtensionString,
                        'a',
                      )))
            ..inners.addAll([
              (Inner()
                ..setExtension(Extend_unittest.innerExtensionString, 'a')),
              Inner()..value = 'value',
            ])
            ..innerMap[0] =
                (Inner()
                  ..setExtension(Extend_unittest.innerExtensionString, 'a'));

      final reparsed = ExtensionRegistry().reparseMessage(original);
      expect(identical(reparsed, original), isTrue);
    },
  );

  test(
    'ExtensionRegistry.reparseMessage reparses extension in deeply nested subfield',
    () {
      final original =
          Outer()
            ..inner =
                (Inner()
                  ..innerMost =
                      (InnerMost()..setExtension(
                        Extend_unittest.innerMostExtensionString,
                        'a',
                      )));

      final withUnknownFields = Outer.fromBuffer(original.writeToBuffer());

      expect(
        withUnknownFields.inner.innerMost.hasExtension(
          Extend_unittest.innerMostExtensionString,
        ),
        isFalse,
      );

      final r =
          ExtensionRegistry()..add(Extend_unittest.innerMostExtensionString);
      final reparsed = r.reparseMessage(withUnknownFields);

      expect(identical(reparsed.inner, withUnknownFields.inner), isFalse);
      expect(
        identical(reparsed.inner.innerMost, withUnknownFields.inner.innerMost),
        isFalse,
      );
      expect(
        reparsed.inner.innerMost.hasExtension(
          Extend_unittest.innerMostExtensionString,
        ),
        isTrue,
      );
    },
  );

  test(
    'ExtensionRegistry.reparseMessage reparses extensions in all nested subfields',
    () {
      final original =
          Outer()
            ..inner =
                (Inner()
                  ..setExtension(Extend_unittest.innerExtensionString, 'a')
                  ..innerMost =
                      (InnerMost()..setExtension(
                        Extend_unittest.innerMostExtensionString,
                        'a',
                      )));

      final withUnknownFields = Outer.fromBuffer(original.writeToBuffer());

      expect(
        withUnknownFields.inner.hasExtension(
          Extend_unittest.innerExtensionString,
        ),
        isFalse,
      );
      expect(
        withUnknownFields.inner.innerMost.hasExtension(
          Extend_unittest.innerMostExtensionString,
        ),
        isFalse,
      );

      final r =
          ExtensionRegistry()..addAll([
            Extend_unittest.innerExtensionString,
            Extend_unittest.innerMostExtensionString,
          ]);
      final reparsed = r.reparseMessage(withUnknownFields);

      expect(identical(reparsed.inner, withUnknownFields.inner), isFalse);
      expect(
        identical(reparsed.inner.innerMost, withUnknownFields.inner.innerMost),
        isFalse,
      );
      expect(
        reparsed.inner.hasExtension(Extend_unittest.innerExtensionString),
        isTrue,
      );
      expect(
        reparsed.inner.innerMost.hasExtension(
          Extend_unittest.innerMostExtensionString,
        ),
        isTrue,
      );
    },
  );

  test(
    'ExtensionRegistry.reparseMessage doesn\'t copy deepest subfield without extensions',
    () {
      final original =
          Outer()
            ..inner =
                (Inner()
                  ..setExtension(Extend_unittest.innerExtensionString, 'a')
                  ..innerMost = InnerMost());

      final withUnknownFields = Outer.fromBuffer(original.writeToBuffer());

      expect(
        withUnknownFields.inner.hasExtension(
          Extend_unittest.innerExtensionString,
        ),
        false,
      );

      final r = ExtensionRegistry()..add(Extend_unittest.innerExtensionString);
      final reparsed = r.reparseMessage(withUnknownFields);

      expect(identical(reparsed.inner, withUnknownFields.inner), isFalse);
      expect(
        identical(reparsed.inner.innerMost, withUnknownFields.inner.innerMost),
        isTrue,
      );
      expect(
        reparsed.inner.hasExtension(Extend_unittest.innerExtensionString),
        isTrue,
      );
    },
  );

  test(
    'ExtensionRegistry.reparseMessage reparses extensions in repeated fields',
    () {
      final original =
          Outer()
            ..inners.addAll([
              (Inner()
                ..setExtension(Extend_unittest.innerExtensionString, 'a')),
              Inner()..value = 'value',
            ]);

      final withUnknownFields = Outer.fromBuffer(original.writeToBuffer());

      expect(
        withUnknownFields.inners.first.hasExtension(
          Extend_unittest.innerExtensionString,
        ),
        isFalse,
      );

      final r = ExtensionRegistry()..add(Extend_unittest.innerExtensionString);
      final reparsed = r.reparseMessage(withUnknownFields);

      expect(
        reparsed.inners[0].hasExtension(Extend_unittest.innerExtensionString),
        isTrue,
      );
      expect(
        identical(withUnknownFields.inners[1], reparsed.inners[1]),
        isTrue,
      );
    },
  );

  test(
    'ExtensionRegistry.reparseMessage reparses extensions in map fields',
    () {
      final original =
          Outer()
            ..innerMap[0] =
                (Inner()
                  ..setExtension(Extend_unittest.innerExtensionString, 'a'))
            ..innerMap[1] = (Inner())
            ..stringMap['hello'] = 'world';

      final withUnknownFields = Outer.fromBuffer(original.writeToBuffer());

      expect(
        withUnknownFields.innerMap[0]!.hasExtension(
          Extend_unittest.innerExtensionString,
        ),
        isFalse,
      );

      final r = ExtensionRegistry()..add(Extend_unittest.innerExtensionString);
      final reparsed = r.reparseMessage(withUnknownFields);

      expect(
        reparsed.innerMap[0]!.hasExtension(
          Extend_unittest.innerExtensionString,
        ),
        isTrue,
      );
      expect(
        identical(withUnknownFields.innerMap[1], reparsed.innerMap[1]),
        isTrue,
      );
      expect(withUnknownFields.stringMap.length, 1);
      expect(reparsed.stringMap.length, 1);
      expect(
        withUnknownFields.stringMap['hello']!,
        reparsed.stringMap['hello']!,
      );
    },
  );

  test('consistent hashCode for reparsed messages with extensions', () {
    final r = ExtensionRegistry()..add(Extend_unittest.outer);
    final m =
        TestAllExtensions()..setExtension(
          Extend_unittest.outer,
          Outer()..inner = (Inner()..value = 'hello'),
        );
    final b = m.writeToBuffer();
    final c = TestAllExtensions.fromBuffer(b);
    final d = r.reparseMessage(c);
    expect(m.hashCode, d.hashCode);
  });

  test('setExtension throws when the extension field is repeated', () {
    final m = Outer();
    m.addExtension(Extend_unittest.extensionRepeated, 'hi');
    expect(() {
      m.setExtension(Extend_unittest.extensionRepeated, 'bye');
    }, throwsArgumentError);
  });

  test('addExtension throws when the extension field is not repeated', () {
    final m = Outer();
    m.setExtension(Extend_unittest.extensionInner, Inner());
    expect(() {
      m.addExtension(Extend_unittest.extensionInner, Inner());
    }, throwsArgumentError);
  });

  test('addExtension throws when the message is frozen', () {
    final m = Outer()..freeze();
    expect(
      () => m.addExtension(Extend_unittest.extensionRepeated, 'hi'),
      throwsUnsupportedError,
    );
  });

  test('setExtension throws when the message is frozen', () {
    final m = Outer()..freeze();
    expect(
      () => m.setExtension(Extend_unittest.extensionInner, Inner()),
      throwsUnsupportedError,
    );
  });

  test(
    'getExtension returns frozen non-repeated value when the parent message is frozen',
    () {
      final m = Outer()..freeze();
      final Inner ext = m.getExtension(Extend_unittest.extensionInner);
      expect(() => ext.value = 'hi', throwsUnsupportedError);
    },
  );

  test(
    'getExtension returns frozen repeated value when the parent message is frozen',
    () {
      final m = Outer()..freeze();
      final List ext = m.getExtension(Extend_unittest.extensionRepeated);
      expect(() => ext.add('hi'), throwsUnsupportedError);
    },
  );

  test(
    'getExtension repeated value turns frozen when the parent message is frozen',
    () {
      final m = Outer();
      final List ext = m.getExtension(Extend_unittest.extensionRepeated);
      ext.add('hi');
      m.freeze();
      expect(() => ext.add('bye'), throwsUnsupportedError);
    },
  );

  test(
    'getExtension repeated value type is right when the field set is frozen',
    () {
      {
        final m = Outer();
        final ext = m.getExtension(Extend_unittest.extensionRepeated);
        expect(ext, isA<PbList<String>>());
      }

      {
        final m = Outer().freeze();
        final ext = m.getExtension(Extend_unittest.extensionRepeated);
        expect(ext, isA<PbList<String>>());
      }
    },
  );
}
