// Copyright (c) 2019, 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 'dart:convert';

import 'package:fixnum/fixnum.dart' show Int64;
import 'package:protobuf/protobuf.dart';
import 'package:test/test.dart';

import 'gen/foo.pb.dart';
import 'gen/google/protobuf/unittest.pb.dart';

void main() {
  group('frozen and tobuilder', () {
    final original =
        Outer()
          ..inner = (Inner()..value = 'foo')
          ..inners.add(Inner()..value = 'repeatedInner')
          ..setExtension(FooExt.inner, Inner()..value = 'extension')
          ..getExtension(
            FooExt.inners,
          ).add(Inner()..value = 'repeatedExtension')
          ..freeze();
    test('can read extensions', () {
      expect(original.getExtension(FooExt.inner).value, 'extension');
      expect(
        original.getExtension(FooExt.inners)[0].value,
        'repeatedExtension',
      );
    });

    test('frozen message cannot be modified', () {
      expect(
        () => original.inner = (Inner()..value = 'bar'),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => original.inner..value = 'bar',
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => original.inners.add(Inner()..value = 'bar'),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    });

    test('extensions cannot be modified', () {
      expect(
        () => original.setExtension(FooExt.inner, Inner()..value = 'bar'),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => original.getExtension(FooExt.inner).value = 'bar',
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => original.getExtension(FooExt.inners).add(Inner()..value = 'bar'),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    });

    final builder = original.toBuilder() as Outer;
    test('builder is a shallow copy', () {
      expect(builder.inner, same(original.inner));
    });
    test('builder extensions are also copied shallowly', () {
      expect(
        builder.getExtension(FooExt.inner),
        same(original.getExtension(FooExt.inner)),
      );
    });

    test('repeated fields are cloned', () {
      expect(builder.inners, isNot(same(original.inners)));
      expect(builder.inners[0], same(original.inners[0]));
    });

    test('repeated extensions are cloned', () {
      expect(
        builder.getExtension(FooExt.inners),
        isNot(same(original.getExtension(FooExt.inners))),
      );
      expect(
        builder.getExtension(FooExt.inners)[0],
        same(original.getExtension(FooExt.inners)[0]),
      );
    });

    test(
      'the builder is only a shallow copy, the nested message is still frozen.',
      () {
        expect(
          () => builder.inner.value = 'bar',
          throwsA(TypeMatcher<UnsupportedError>()),
        );
      },
    );
    test('the builder is mutable', () {
      builder.inner = (Inner()..value = 'zop');
      expect(builder.inner.value, 'zop');
      builder.inners.add(Inner()..value = 'bob');
      expect(builder.inners.length, 2);
      builder.setExtension(FooExt.inner, Inner()..value = 'nob');
      expect(builder.getExtension(FooExt.inner).value, 'nob');
      builder.getExtension(FooExt.inners).add(Inner()..value = 'rob');
      expect(builder.getExtension(FooExt.inners).length, 2);
    });
    test('newly created `Inner` is mutable', () {
      builder.inner.value = 'bar';
      expect(builder.inner.value, 'bar');
    });
  });

  group('map properties behave correctly', () {
    late OuterWithMap original;
    late OuterWithMap outerBuilder;
    setUp(() {
      original =
          OuterWithMap()
            ..innerMap[1] = (Inner()..value = 'mapInner')
            ..freeze();
      outerBuilder = original.toBuilder() as OuterWithMap;
    });
    test('map fields are cloned', () {
      expect(outerBuilder.innerMap, isNot(same(original.innerMap)));
      expect(outerBuilder.innerMap[1], same(original.innerMap[1]));
    });
    test('the builder is mutable', () {
      outerBuilder.innerMap[1] = (Inner()..value = 'mob');
      expect(outerBuilder.innerMap[1]!.value, 'mob');
    });
  });

  group('frozen unknown fields', () {
    late Inner inner;
    late TestEmptyMessage emptyMessage;
    late int tagNumber;
    late UnknownFieldSet unknownFieldSet;
    late UnknownFieldSetField field;

    setUp(() {
      inner = Inner()..value = 'bob';
      emptyMessage = TestEmptyMessage.fromBuffer(inner.writeToBuffer());
      tagNumber = inner.getTagNumber('value')!;
      unknownFieldSet = emptyMessage.unknownFields;
      field = unknownFieldSet.getField(tagNumber)!;
    });

    test('can read from a frozen unknown fieldset', () {
      expect(unknownFieldSet.hasField(tagNumber), isTrue);
      expect(field.lengthDelimited[0], utf8.encode(inner.value));

      emptyMessage.freeze();
      unknownFieldSet = emptyMessage.unknownFields;
      field = unknownFieldSet.getField(tagNumber)!;

      expect(unknownFieldSet.hasField(tagNumber), isTrue);
      expect(field.lengthDelimited[0], utf8.encode(inner.value));
    });

    test('can add fields to a builder with unknown fields', () {
      emptyMessage.freeze();
      final builder = emptyMessage.toBuilder() as TestEmptyMessage;

      builder.unknownFields.addField(
        2,
        UnknownFieldSetField()..fixed32s.add(42),
      );
      expect(builder.unknownFields.getField(2)!.fixed32s[0], 42);
    });

    test('cannot mutate already added UnknownFieldSetField on builder', () {
      emptyMessage.freeze();
      final builder = emptyMessage.toBuilder() as TestEmptyMessage;

      expect(
        () =>
            builder.unknownFields.getField(1)!.lengthDelimited[0] = utf8.encode(
              'alice',
            ),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    });

    test('cannot add to a frozen UnknownFieldSetField', () {
      emptyMessage.freeze();

      expect(
        () => field.addFixed32(1),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.fixed32s.add(1),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.addFixed64(Int64(1)),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.fixed64s.add(Int64(1)),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.addLengthDelimited([1]),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.lengthDelimited.add([1]),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.addGroup(unknownFieldSet.clone()),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.groups.add(unknownFieldSet.clone()),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.addVarint(Int64(1)),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => field.varints.add(Int64(1)),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    });

    test('cannot add or merge field to a frozen UnknownFieldSet', () {
      emptyMessage.freeze();

      expect(
        () => unknownFieldSet.addField(2, field),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => unknownFieldSet.mergeField(2, field),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    });

    test('cannot merge message into a frozen UnknownFieldSet', () {
      emptyMessage.freeze();
      final other = emptyMessage.deepCopy();

      expect(
        () => emptyMessage.mergeFromBuffer(other.writeToBuffer()),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
      expect(
        () => emptyMessage.mergeFromMessage(other),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    });

    test('cannot add a field to a frozen UnknownFieldSet', () {
      emptyMessage.freeze();

      expect(
        () => unknownFieldSet.addField(tagNumber, field),
        throwsA(TypeMatcher<UnsupportedError>()),
      );
    });
  });
}
