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

// This files contains message codec tests that are supported both on the Web
// and in the VM. For VM-only tests see message_codecs_vm_test.dart.

import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/foundation.dart' show WriteBuffer;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'message_codecs_testing.dart';

void main() {
  group('Binary codec', () {
    const MessageCodec<ByteData?> binary = BinaryCodec();
    test('should encode and decode simple messages', () {
      checkEncodeDecode<ByteData?>(binary, null);
      checkEncodeDecode<ByteData?>(binary, ByteData(0));
      checkEncodeDecode<ByteData?>(binary, ByteData(4)..setInt32(0, -7));
    });
  });
  group('String codec', () {
    const MessageCodec<String?> string = StringCodec();
    test('should encode and decode simple messages', () {
      checkEncodeDecode<String?>(string, null);
      checkEncodeDecode<String?>(string, '');
      checkEncodeDecode<String?>(string, 'hello');
      checkEncodeDecode<String?>(string, 'special chars >\u263A\u{1F602}<');
    });
    test('ByteData with offset', () {
      const MessageCodec<String?> string = StringCodec();
      final ByteData helloWorldByteData = string.encodeMessage('hello world')!;
      final ByteData helloByteData = string.encodeMessage('hello')!;
      final ByteData offsetByteData = ByteData.view(
        helloWorldByteData.buffer,
        helloByteData.lengthInBytes,
        helloWorldByteData.lengthInBytes - helloByteData.lengthInBytes,
      );

      expect(string.decodeMessage(offsetByteData), ' world');
    });
  });
  group('Standard method codec', () {
    const MethodCodec method = StandardMethodCodec();
    const StandardMessageCodec messageCodec = StandardMessageCodec();

    test('Should encode and decode objects produced from codec', () {
      final ByteData? data = messageCodec.encodeMessage(<Object, Object>{
        'foo': true,
        3: 'fizz',
      });

      expect(messageCodec.decodeMessage(data), <Object?, Object?>{
        'foo': true,
        3: 'fizz',
      });
    });

    test('should decode error envelope without native stacktrace', () {
      final ByteData errorData = method.encodeErrorEnvelope(
        code: 'errorCode',
        message: 'errorMessage',
        details: 'errorDetails',
      );
      expect(
        () => method.decodeEnvelope(errorData),
        throwsA(predicate(
          (PlatformException e) =>
              e.code == 'errorCode' &&
              e.message == 'errorMessage' &&
              e.details == 'errorDetails',
        )),
      );
    });

    test('should decode error envelope with native stacktrace.', () {
      final WriteBuffer buffer = WriteBuffer();
      buffer.putUint8(1);
      messageCodec.writeValue(buffer, 'errorCode');
      messageCodec.writeValue(buffer, 'errorMessage');
      messageCodec.writeValue(buffer, 'errorDetails');
      messageCodec.writeValue(buffer, 'errorStacktrace');
      final ByteData errorData = buffer.done();
      expect(
        () => method.decodeEnvelope(errorData),
        throwsA(predicate((PlatformException e) => e.stacktrace == 'errorStacktrace')),
      );
    });

    test('should allow null error message,', () {
      final ByteData errorData = method.encodeErrorEnvelope(
        code: 'errorCode',
        details: 'errorDetails',
      );
      expect(
        () => method.decodeEnvelope(errorData),
        throwsA(
          predicate((PlatformException e) {
            return e.code == 'errorCode' &&
              e.message == null &&
              e.details == 'errorDetails';
          }),
        ),
      );
    });
  });
  group('Json method codec', () {
    const JsonCodec json = JsonCodec();
    const StringCodec stringCodec = StringCodec();
    const JSONMethodCodec jsonMethodCodec = JSONMethodCodec();
    test('should decode error envelope without native stacktrace', () {
      final ByteData errorData = jsonMethodCodec.encodeErrorEnvelope(
        code: 'errorCode',
        message: 'errorMessage',
        details: 'errorDetails',
      );
      expect(
        () => jsonMethodCodec.decodeEnvelope(errorData),
        throwsA(predicate(
          (PlatformException e) =>
            e.code == 'errorCode' &&
            e.message == 'errorMessage' &&
            e.details == 'errorDetails',
        )),
      );
    });
    test('should decode error envelope with native stacktrace.', () {
      final ByteData? errorData = stringCodec.encodeMessage(json.encode(<dynamic>[
        'errorCode',
        'errorMessage',
        'errorDetails',
        'errorStacktrace',
      ]));
      expect(
        () => jsonMethodCodec.decodeEnvelope(errorData!),
        throwsA(predicate((PlatformException e) => e.stacktrace == 'errorStacktrace')),
      );
    });
  });
  group('JSON message codec', () {
    const MessageCodec<dynamic> json = JSONMessageCodec();
    test('should encode and decode simple messages', () {
      checkEncodeDecode<dynamic>(json, null);
      checkEncodeDecode<dynamic>(json, true);
      checkEncodeDecode<dynamic>(json, false);
      checkEncodeDecode<dynamic>(json, 7);
      checkEncodeDecode<dynamic>(json, -7);
      checkEncodeDecode<dynamic>(json, 98742923489);
      checkEncodeDecode<dynamic>(json, -98742923489);
      checkEncodeDecode<dynamic>(json, 3.14);
      checkEncodeDecode<dynamic>(json, '');
      checkEncodeDecode<dynamic>(json, 'hello');
      checkEncodeDecode<dynamic>(json, 'special chars >\u263A\u{1F602}<');
    });
    test('should encode and decode composite message', () {
      final List<dynamic> message = <dynamic>[
        null,
        true,
        false,
        -707,
        -7000000007,
        -3.14,
        '',
        'hello',
        <dynamic>['nested', <dynamic>[]],
        <dynamic, dynamic>{'a': 'nested', 'b': <dynamic, dynamic>{}},
        'world',
      ];
      checkEncodeDecode<dynamic>(json, message);
    });
  });
  group('Standard message codec', () {
    const MessageCodec<dynamic> standard = StandardMessageCodec();
    test('should encode sizes correctly at boundary cases', () {
      checkEncoding<dynamic>(
        standard,
        Uint8List(253),
        <int>[8, 253, ...List<int>.filled(253, 0)],
      );
      checkEncoding<dynamic>(
        standard,
        Uint8List(254),
        <int>[8, 254, 254, 0, ...List<int>.filled(254, 0)],
      );
      checkEncoding<dynamic>(
        standard,
        Uint8List(0xffff),
        <int>[8, 254, 0xff, 0xff, ...List<int>.filled(0xffff, 0)],
      );
      checkEncoding<dynamic>(
        standard,
        Uint8List(0xffff + 1),
        <int>[8, 255, 0, 0, 1, 0, ...List<int>.filled(0xffff + 1, 0)],
      );
    });
    test('should encode and decode simple messages', () {
      checkEncodeDecode<dynamic>(standard, null);
      checkEncodeDecode<dynamic>(standard, true);
      checkEncodeDecode<dynamic>(standard, false);
      checkEncodeDecode<dynamic>(standard, 7);
      checkEncodeDecode<dynamic>(standard, -7);
      checkEncodeDecode<dynamic>(standard, 98742923489);
      checkEncodeDecode<dynamic>(standard, -98742923489);
      checkEncodeDecode<dynamic>(standard, 3.14);
      checkEncodeDecode<dynamic>(standard, double.infinity);
      checkEncodeDecode<dynamic>(standard, double.nan);
      checkEncodeDecode<dynamic>(standard, '');
      checkEncodeDecode<dynamic>(standard, 'hello');
      checkEncodeDecode<dynamic>(standard, 'special chars >\u263A\u{1F602}<');
    });
    test('should encode and decode composite message', () {
      final List<dynamic> message = <dynamic>[
        null,
        true,
        false,
        -707,
        -7000000007,
        -3.14,
        '',
        'hello',
        Uint8List.fromList(<int>[0xBA, 0x5E, 0xBA, 0x11]),
        Int32List.fromList(<int>[-0x7fffffff - 1, 0, 0x7fffffff]),
        null, // ensures the offset of the following list is unaligned.
        null, // ensures the offset of the following list is unaligned.
        Float64List.fromList(<double>[
          double.negativeInfinity,
          -double.maxFinite,
          -double.minPositive,
          -0.0,
          0.0,
          double.minPositive,
          double.maxFinite,
          double.infinity,
          double.nan,
        ]),
        Float32List.fromList(<double>[
          double.negativeInfinity,
          -double.maxFinite,
          -double.minPositive,
          -0.0,
          0.0,
          double.minPositive,
          double.maxFinite,
          double.infinity,
          double.nan,
        ]),
        <dynamic>['nested', <dynamic>[]],
        <dynamic, dynamic>{'a': 'nested', null: <dynamic, dynamic>{}},
        'world',
      ];
      checkEncodeDecode<dynamic>(standard, message);
    });
    test('should align doubles to 8 bytes', () {
      checkEncoding<dynamic>(
        standard,
        1.0,
        <int>[
          6,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
          0,
          0xf0,
          0x3f,
        ],
      );
    });
  });

  test('toString works as intended', () async {
    const MethodCall methodCall = MethodCall('sample method');
    final PlatformException platformException = PlatformException(code: '100');
    final MissingPluginException missingPluginException = MissingPluginException();

    expect(methodCall.toString(), 'MethodCall(sample method, null)');
    expect(platformException.toString(), 'PlatformException(100, null, null, null)');
    expect(missingPluginException.toString(), 'MissingPluginException(null)');
  });
}
