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

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

import 'gen/foo.pb.dart' as foo;
import 'gen/google/protobuf/unittest.pb.dart';
import 'gen/map_enum_value.pb.dart';
import 'src/test_util.dart';

void main() {
  final testAllJsonTypes =
      '{"1":101,"2":"102","3":103,"4":"104",'
      '"5":105,"6":"106","7":107,"8":"108","9":109,"10":"110","11":111,'
      '"12":112,"13":true,"14":"115","15":"MTE2","16":{"17":117},'
      '"18":{"1":118},"19":{"1":119},"20":{"1":120},"21":3,"22":6,"23":9,'
      '"24":"124","25":"125","31":[201,301],"32":["202","302"],'
      '"33":[203,303],"34":["204","304"],"35":[205,305],"36":["206","306"],'
      '"37":[207,307],"38":["208","308"],"39":[209,309],"40":["210","310"],'
      '"41":[211,311],"42":[212,312],"43":[true,false],'
      '"44":["215","315"],"45":["MjE2","MzE2"],"46":[{"47":217},{"47":317}],'
      '"48":[{"1":218},{"1":318}],"49":[{"1":219},{"1":319}],'
      '"50":[{"1":220},{"1":320}],"51":[2,3],"52":[5,6],"53":[8,9],'
      '"54":["224","324"],"55":["225","325"],"61":401,"62":"402","63":403,'
      '"64":"404","65":405,"66":"406","67":407,"68":"408","69":409,'
      '"70":"410","71":411,"72":412,"73":false,"74":"415","75":"NDE2",'
      '"81":1,"82":4,"83":7,"84":"424","85":"425"}';

  /// Checks that the message, once serialized to JSON, matches
  /// [testAllJsonTypes] massaged with `replaceAll(from, to)`.
  Matcher expectedJson(String from, String to) {
    final expectedJson = testAllJsonTypes.replaceAll(from, to);
    return predicate(
      (GeneratedMessage message) => message.writeToJson() == expectedJson,
      'Incorrect output',
    );
  }

  test('testUnsignedOutput', () {
    final message = TestAllTypes();
    // These values selected because:
    // (1) large enough to set the sign bit
    // (2) don't set all of the first 10 bits under the sign bit
    // (3) are near each other
    message.optionalUint64 = Int64.parseHex('f0000000ffff0000');
    message.optionalFixed64 = Int64.parseHex('f0000000ffff0001');

    final expectedJsonValue =
        '{"4":"17293822573397606400","8":"17293822573397606401"}';
    expect(message.writeToJson(), expectedJsonValue);
  });

  test('testOutput', () {
    expect(getAllSet().writeToJson(), testAllJsonTypes);

    // Test empty list.
    expect(
      getAllSet()..repeatedBool.clear(),
      expectedJson('"43":[true,false],', ''),
    );

    // Test negative number.
    expect(
      getAllSet()..optionalInt32 = -1234567,
      expectedJson(':101,', ':-1234567,'),
    );

    // All 64-bit numbers are quoted.
    expect(
      getAllSet()..optionalInt64 = make64(0, 0x200000),
      expectedJson(':"102",', ':"9007199254740992",'),
    );
    expect(
      getAllSet()..optionalInt64 = make64(1, 0x200000),
      expectedJson(':"102",', ':"9007199254740993",'),
    );
    expect(
      getAllSet()..optionalInt64 = -make64(0, 0x200000),
      expectedJson(':"102",', ':"-9007199254740992",'),
    );
    expect(
      getAllSet()..optionalInt64 = -make64(1, 0x200000),
      expectedJson(':"102",', ':"-9007199254740993",'),
    );

    // Quotes, backslashes, and control characters in strings are quoted.
    expect(
      getAllSet()..optionalString = 'a\u0000b\u0001cd\\e"fg',
      expectedJson(':"115",', ':"a\\u0000b\\u0001cd\\\\e\\"fg",'),
    );
  });

  test('testBase64Encode', () {
    expect(
      getAllSet()..optionalBytes = 'Hello, world'.codeUnits,
      expectedJson(':"MTE2",', ':"SGVsbG8sIHdvcmxk",'),
    );

    expect(
      getAllSet()..optionalBytes = 'Hello, world!'.codeUnits,
      expectedJson(':"MTE2",', ':"SGVsbG8sIHdvcmxkIQ==",'),
    );

    expect(
      getAllSet()..optionalBytes = 'Hello, world!!'.codeUnits,
      expectedJson(':"MTE2",', ':"SGVsbG8sIHdvcmxkISE=",'),
    );

    // An empty list should not appear in the output.
    expect(getAllSet()..optionalBytes = [], expectedJson('"15":"MTE2",', ''));

    expect(
      getAllSet()..optionalBytes = 'a'.codeUnits,
      expectedJson(':"MTE2",', ':"YQ==",'),
    );
  });

  test('testBase64Decode', () {
    String optionalBytes(String from, String to) {
      final json = testAllJsonTypes.replaceAll(from, to);
      return String.fromCharCodes(TestAllTypes.fromJson(json).optionalBytes);
    }

    expect(optionalBytes(':"MTE2",', ':"SGVsbG8sIHdvcmxk",'), 'Hello, world');

    expect(
      optionalBytes(':"MTE2",', ':"SGVsbG8sIHdvcmxkIQ==",'),
      'Hello, world!',
    );

    expect(
      optionalBytes(':"MTE2",', ':"SGVsbG8sIHdvcmxkISE=",'),
      'Hello, world!!',
    );

    // Remove optionalBytes tag, reads back as empty list, hence empty string.
    expect(optionalBytes('"15":"MTE2",', ''), isEmpty);

    // Keep optionalBytes tag, set data to empty string, get back empty list.
    expect(optionalBytes(':"MTE2",', ':"",'), isEmpty);

    expect(optionalBytes(':"MTE2",', ':"YQ==",'), 'a');
  });

  test('testParseUnsigned', () {
    final parsed = TestAllTypes.fromJson(
      '{"4":"17293822573397606400","8":"17293822573397606401"}',
    );
    final expected = TestAllTypes();
    expected.optionalUint64 = Int64.parseHex('f0000000ffff0000');
    expected.optionalFixed64 = Int64.parseHex('f0000000ffff0001');

    expect(parsed, expected);
  });

  group('testConvertDouble', () {
    test('WithDecimal', () {
      final json = '{"12":1.2}';
      final proto = TestAllTypes()..optionalDouble = 1.2;
      expect(TestAllTypes.fromJson(json), proto);
      expect(proto.writeToJson(), json);
    });

    test('WholeNumber', () {
      final json = '{"12":5}';
      final proto = TestAllTypes()..optionalDouble = 5.0;
      expect(TestAllTypes.fromJson(json), proto);
      expect(proto.writeToJson(), json);
    });

    test('Infinity', () {
      final json = '{"12":"Infinity"}';
      final proto = TestAllTypes()..optionalDouble = double.infinity;
      expect(TestAllTypes.fromJson(json), proto);
      expect(proto.writeToJson(), json);
    });

    test('NegativeInfinity', () {
      final json = '{"12":"-Infinity"}';
      final proto = TestAllTypes()..optionalDouble = double.negativeInfinity;
      expect(TestAllTypes.fromJson(json), proto);
      expect(proto.writeToJson(), json);
    });
  });

  test('testParseUnsignedLegacy', () {
    final parsed = TestAllTypes.fromJson(
      '{"4":"-1152921500311945216","8":"-1152921500311945215"}',
    );
    final expected = TestAllTypes();
    expected.optionalUint64 = Int64.parseHex('f0000000ffff0000');
    expected.optionalFixed64 = Int64.parseHex('f0000000ffff0001');

    expect(parsed, expected);
  });

  test('testFixed32IntNegative', () {
    var message = foo.Inner.fromJson('{"5": -1}');
    expect(message.count, 4294967295);
    message = foo.Inner.fromJson('{"5": -2080294357}');
    expect(message.count, 2214672939);
  });

  test('testUint32Negative', () {
    var message = foo.Inner.fromJson('{"6": -1}');
    expect(message.countUint32, 4294967295);
    message = foo.Inner.fromJson('{"6": -2080294357}');
    expect(message.countUint32, 2214672939);
  });

  test('testParse', () {
    expect(TestAllTypes.fromJson(testAllJsonTypes), getAllSet());
  });

  test('testExtensionsOutput', () {
    expect(getAllExtensionsSet().writeToJson(), testAllJsonTypes);
  });

  test('testExtensionsParse', () {
    final registry = getExtensionRegistry();
    expect(
      TestAllExtensions.fromJson(testAllJsonTypes, registry),
      getAllExtensionsSet(),
    );
  });

  test('testUnknownEnumValueInOptionalField', () {
    // optional NestedEnum optional_nested_enum = 21;
    final message = TestAllTypes.fromJson('{"21": 4}');
    // 4 is an unknown value.
    expect(message.optionalNestedEnum, equals(TestAllTypes_NestedEnum.FOO));
  });

  test('testUnknownEnumValueInRepeatedField', () {
    // repeated NestedEnum repeated_nested_enum = 51;
    var message = TestAllTypes.fromJson('{"51": [4]}');
    // 4 is an unknown value, which should default to the enum's default value.
    expect(message.repeatedNestedEnum, equals([TestAllTypes_NestedEnum.FOO]));

    // 1 (FOO) and 2 (BAR) are known values. All unknowns should fill in to
    // the default enum value (FOO).
    message = TestAllTypes.fromJson('{"51": [1, 4, 2, 4, 1, 4]}');
    expect(
      message.repeatedNestedEnum,
      equals([
        TestAllTypes_NestedEnum.FOO,
        TestAllTypes_NestedEnum.FOO,
        TestAllTypes_NestedEnum.BAR,
        TestAllTypes_NestedEnum.FOO,
        TestAllTypes_NestedEnum.FOO,
        TestAllTypes_NestedEnum.FOO,
      ]),
    );
  });

  test('testUnknownEnumValueInMapField', () {
    final key = 'new_field';
    // Only 0 and 1 are known enum values.
    final message = MapEnumValue()..mergeFromJson('{"1":[{"1":"$key","2":2}]}');
    expect(message.values[key], equals(MapEnumValue_NestedEnum.UNKNOWN));
  });
}
