// 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 'dart:async';
import 'dart:convert';

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

void main() {
  group('encoder', () {
    test("doesn't percent-encode unreserved characters", () {
      var safeChars = 'abcdefghijklmnopqrstuvwxyz'
          'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
          '0123456789-._~';
      expect(percent.encode([...safeChars.codeUnits]), equals(safeChars));
    });

    test('percent-encodes reserved ASCII characters', () {
      expect(percent.encode([...' `{@[,/^}\x7f\x00%'.codeUnits]),
          equals('%20%60%7B%40%5B%2C%2F%5E%7D%7F%00%25'));
    });

    test('percent-encodes non-ASCII characters', () {
      expect(percent.encode([0x80, 0xFF]), equals('%80%FF'));
    });

    test('mixes encoded and unencoded characters', () {
      expect(percent.encode([...'a+b=\x80'.codeUnits]), equals('a%2Bb%3D%80'));
    });

    group('with chunked conversion', () {
      test('percent-encodes byte arrays', () {
        var results = <String>[];
        var controller = StreamController<String>(sync: true);
        controller.stream.listen(results.add);
        var sink = percent.encoder.startChunkedConversion(controller.sink);

        sink.add([...'a+b=\x80'.codeUnits]);
        expect(results, equals(['a%2Bb%3D%80']));

        sink.add([0x00, 0x01, 0xfe, 0xff]);
        expect(results, equals(['a%2Bb%3D%80', '%00%01%FE%FF']));
      });

      test('handles empty and single-byte lists', () {
        var results = <String>[];
        var controller = StreamController<String>(sync: true);
        controller.stream.listen(results.add);
        var sink = percent.encoder.startChunkedConversion(controller.sink);

        sink.add([]);
        expect(results, equals(['']));

        sink.add([0x00]);
        expect(results, equals(['', '%00']));

        sink.add([]);
        expect(results, equals(['', '%00', '']));
      });
    });

    test('rejects non-bytes', () {
      expect(() => percent.encode([0x100]), throwsFormatException);

      var sink =
          percent.encoder.startChunkedConversion(StreamController(sync: true));
      expect(() => sink.add([0x100]), throwsFormatException);
    });
  });

  group('decoder', () {
    test('converts percent-encoded strings to byte arrays', () {
      expect(
          percent.decode('a%2Bb%3D%801'), equals([...'a+b=\x801'.codeUnits]));
    });

    test('supports lowercase letters', () {
      expect(percent.decode('a%2bb%3d%80'), equals([...'a+b=\x80'.codeUnits]));
    });

    test('supports more aggressive encoding', () {
      expect(percent.decode('%61%2E%5A'), equals([...'a.Z'.codeUnits]));
    });

    test('supports less aggressive encoding', () {
      var chars = ' `{@[,/^}\x7F\x00';
      expect(percent.decode(chars), equals([...chars.codeUnits]));
    });

    group('with chunked conversion', () {
      late List<List<int>> results;
      late StringConversionSink sink;
      setUp(() {
        results = [];
        var controller = StreamController<List<int>>(sync: true);
        controller.stream.listen(results.add);
        sink = percent.decoder.startChunkedConversion(controller.sink);
      });

      test('converts percent to byte arrays', () {
        sink.add('a%2Bb%3D%801');
        expect(
            results,
            equals([
              [...'a+b=\x801'.codeUnits]
            ]));

        sink.add('%00%01%FE%FF');
        expect(
            results,
            equals([
              [...'a+b=\x801'.codeUnits],
              [0x00, 0x01, 0xfe, 0xff]
            ]));
      });

      test('supports trailing percents and digits split across chunks', () {
        sink.add('ab%');
        expect(
            results,
            equals([
              [...'ab'.codeUnits]
            ]));

        sink.add('2');
        expect(
            results,
            equals([
              [...'ab'.codeUnits]
            ]));

        sink.add('0cd%2');
        expect(
            results,
            equals([
              [...'ab'.codeUnits],
              [...' cd'.codeUnits]
            ]));

        sink.add('0');
        expect(
            results,
            equals([
              [...'ab'.codeUnits],
              [...' cd'.codeUnits],
              [...' '.codeUnits]
            ]));
      });

      test('supports empty strings', () {
        sink.add('');
        expect(results, isEmpty);

        sink.add('%');
        expect(results, equals([<Never>[]]));

        sink.add('');
        expect(results, equals([<Never>[]]));

        sink.add('2');
        expect(results, equals([<Never>[]]));

        sink.add('');
        expect(results, equals([<Never>[]]));

        sink.add('0');
        expect(
            results,
            equals([
              <Never>[],
              [0x20]
            ]));
      });

      test('rejects dangling % detected in close()', () {
        sink.add('ab%');
        expect(
            results,
            equals([
              [...'ab'.codeUnits]
            ]));
        expect(() => sink.close(), throwsFormatException);
      });

      test('rejects dangling digit detected in close()', () {
        sink.add('ab%2');
        expect(
            results,
            equals([
              [...'ab'.codeUnits]
            ]));
        expect(() => sink.close(), throwsFormatException);
      });

      test('rejects danging % detected in addSlice()', () {
        sink.addSlice('ab%', 0, 3, false);
        expect(
            results,
            equals([
              [...'ab'.codeUnits]
            ]));

        expect(() => sink.addSlice('ab%', 0, 3, true), throwsFormatException);
      });

      test('rejects danging digit detected in addSlice()', () {
        sink.addSlice('ab%2', 0, 3, false);
        expect(
            results,
            equals([
              [...'ab'.codeUnits]
            ]));

        expect(() => sink.addSlice('ab%2', 0, 3, true), throwsFormatException);
      });
    });

    group('rejects non-ASCII character', () {
      for (var char in ['\u0141', '\u{10041}']) {
        test('"$char"', () {
          expect(() => percent.decode('a$char'), throwsFormatException);
          expect(() => percent.decode('${char}a'), throwsFormatException);

          var sink = percent.decoder
              .startChunkedConversion(StreamController(sync: true));
          expect(() => sink.add(char), throwsFormatException);
        });
      }
    });

    test('rejects % followed by non-hex', () {
      expect(() => percent.decode('%z2'), throwsFormatException);
      expect(() => percent.decode('%2z'), throwsFormatException);
    });

    test('rejects dangling % detected in convert()', () {
      expect(() => percent.decode('ab%'), throwsFormatException);
    });

    test('rejects dangling digit detected in convert()', () {
      expect(() => percent.decode('ab%2'), throwsFormatException);
    });
  });
}
