// Copyright (c) 2016, 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:http_parser/http_parser.dart';

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

void main() {
  group("encoder", () {
    test("adds a header to the chunk of bytes", () {
      expect(chunkedCoding.encode([1, 2, 3]),
          equals([$3, $cr, $lf, 1, 2, 3, $cr, $lf, $0, $cr, $lf, $cr, $lf]));
    });

    test("uses hex for chunk size", () {
      var data = new Iterable<int>.generate(0xA7).toList();
      expect(
          chunkedCoding.encode(data),
          equals([$a, $7, $cr, $lf]
            ..addAll(data)
            ..addAll([$cr, $lf, $0, $cr, $lf, $cr, $lf])));
    });

    test("just generates a footer for an empty input", () {
      expect(chunkedCoding.encode([]), equals([$0, $cr, $lf, $cr, $lf]));
    });

    group("with chunked conversion", () {
      List<List<int>> results;
      ByteConversionSink sink;
      setUp(() {
        results = [];
        var controller = new StreamController<List<int>>(sync: true);
        controller.stream.listen(results.add);
        sink = chunkedCoding.encoder.startChunkedConversion(controller.sink);
      });

      test("adds headers to each chunk of bytes", () {
        sink.add([1, 2, 3, 4]);
        expect(
            results,
            equals([
              [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf]
            ]));

        sink.add([5, 6, 7]);
        expect(
            results,
            equals([
              [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf],
              [$3, $cr, $lf, 5, 6, 7, $cr, $lf],
            ]));

        sink.close();
        expect(
            results,
            equals([
              [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf],
              [$3, $cr, $lf, 5, 6, 7, $cr, $lf],
              [$0, $cr, $lf, $cr, $lf],
            ]));
      });

      test("handles empty chunks", () {
        sink.add([]);
        expect(results, equals([[]]));

        sink.add([1, 2, 3]);
        expect(
            results,
            equals([
              [],
              [$3, $cr, $lf, 1, 2, 3, $cr, $lf]
            ]));

        sink.add([]);
        expect(
            results,
            equals([
              [],
              [$3, $cr, $lf, 1, 2, 3, $cr, $lf],
              []
            ]));

        sink.close();
        expect(
            results,
            equals([
              [],
              [$3, $cr, $lf, 1, 2, 3, $cr, $lf],
              [],
              [$0, $cr, $lf, $cr, $lf],
            ]));
      });

      group("addSlice()", () {
        test("adds bytes from the specified slice", () {
          sink.addSlice([1, 2, 3, 4, 5], 1, 4, false);
          expect(
              results,
              equals([
                [$3, $cr, $lf, 2, 3, 4, $cr, $lf]
              ]));
        });

        test("doesn't add a header if the slice is empty", () {
          sink.addSlice([1, 2, 3, 4, 5], 1, 1, false);
          expect(results, equals([[]]));
        });

        test("adds a footer if isLast is true", () {
          sink.addSlice([1, 2, 3, 4, 5], 1, 4, true);
          expect(
              results,
              equals([
                [$3, $cr, $lf, 2, 3, 4, $cr, $lf, $0, $cr, $lf, $cr, $lf]
              ]));

          // Setting isLast shuld close the sink.
          expect(() => sink.add([]), throwsStateError);
        });

        group("disallows", () {
          test("start < 0", () {
            expect(() => sink.addSlice([1, 2, 3, 4, 5], -1, 4, false),
                throwsRangeError);
          });

          test("start > end", () {
            expect(() => sink.addSlice([1, 2, 3, 4, 5], 3, 2, false),
                throwsRangeError);
          });

          test("end > length", () {
            expect(() => sink.addSlice([1, 2, 3, 4, 5], 1, 10, false),
                throwsRangeError);
          });
        });
      });
    });
  });

  group("decoder", () {
    test("parses chunked data", () {
      expect(
          chunkedCoding.decode([
            $3,
            $cr,
            $lf,
            1,
            2,
            3,
            $cr,
            $lf,
            $4,
            $cr,
            $lf,
            4,
            5,
            6,
            7,
            $cr,
            $lf,
            $0,
            $cr,
            $lf,
            $cr,
            $lf,
          ]),
          equals([1, 2, 3, 4, 5, 6, 7]));
    });

    test("parses hex size", () {
      var data = new Iterable<int>.generate(0xA7).toList();
      expect(
          chunkedCoding.decode([$a, $7, $cr, $lf]
            ..addAll(data)
            ..addAll([$cr, $lf, $0, $cr, $lf, $cr, $lf])),
          equals(data));
    });

    test("parses capital hex size", () {
      var data = new Iterable<int>.generate(0xA7).toList();
      expect(
          chunkedCoding.decode([$A, $7, $cr, $lf]
            ..addAll(data)
            ..addAll([$cr, $lf, $0, $cr, $lf, $cr, $lf])),
          equals(data));
    });

    test("parses an empty message", () {
      expect(chunkedCoding.decode([$0, $cr, $lf, $cr, $lf]), isEmpty);
    });

    group("disallows a message", () {
      test("that ends without any input", () {
        expect(() => chunkedCoding.decode([]), throwsFormatException);
      });

      test("that ends after the size", () {
        expect(() => chunkedCoding.decode([$a]), throwsFormatException);
      });

      test("that ends after CR", () {
        expect(() => chunkedCoding.decode([$a, $cr]), throwsFormatException);
      });

      test("that ends after LF", () {
        expect(
            () => chunkedCoding.decode([$a, $cr, $lf]), throwsFormatException);
      });

      test("that ends after insufficient bytes", () {
        expect(() => chunkedCoding.decode([$a, $cr, $lf, 1, 2, 3]),
            throwsFormatException);
      });

      test("that ends after a chunk's bytes", () {
        expect(() => chunkedCoding.decode([$1, $cr, $lf, 1]),
            throwsFormatException);
      });

      test("that ends after a chunk's CR", () {
        expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr]),
            throwsFormatException);
      });

      test("that ends atfter a chunk's LF", () {
        expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr, $lf]),
            throwsFormatException);
      });

      test("that ends after the empty chunk", () {
        expect(
            () => chunkedCoding.decode([$0, $cr, $lf]), throwsFormatException);
      });

      test("that ends after the closing CR", () {
        expect(() => chunkedCoding.decode([$0, $cr, $lf, $cr]),
            throwsFormatException);
      });

      test("with a chunk without a size", () {
        expect(() => chunkedCoding.decode([$cr, $lf, $0, $cr, $lf, $cr, $lf]),
            throwsFormatException);
      });

      test("with a chunk with a non-hex size", () {
        expect(
            () => chunkedCoding.decode([$q, $cr, $lf, $0, $cr, $lf, $cr, $lf]),
            throwsFormatException);
      });
    });

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

      test("decodes each chunk of bytes", () {
        sink.add([$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf]);
        expect(
            results,
            equals([
              [1, 2, 3, 4]
            ]));

        sink.add([$3, $cr, $lf, 5, 6, 7, $cr, $lf]);
        expect(
            results,
            equals([
              [1, 2, 3, 4],
              [5, 6, 7]
            ]));

        sink.add([$0, $cr, $lf, $cr, $lf]);
        sink.close();
        expect(
            results,
            equals([
              [1, 2, 3, 4],
              [5, 6, 7]
            ]));
      });

      test("handles empty chunks", () {
        sink.add([]);
        expect(results, isEmpty);

        sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]);
        expect(
            results,
            equals([
              [1, 2, 3]
            ]));

        sink.add([]);
        expect(
            results,
            equals([
              [1, 2, 3]
            ]));

        sink.add([$0, $cr, $lf, $cr, $lf]);
        sink.close();
        expect(
            results,
            equals([
              [1, 2, 3]
            ]));
      });

      test("throws if the sink is closed before the message is done", () {
        sink.add([$3, $cr, $lf, 1, 2, 3]);
        expect(() => sink.close(), throwsFormatException);
      });

      group("preserves state when a byte array ends", () {
        test("within chunk size", () {
          sink.add([$a]);
          expect(results, isEmpty);

          var data = new Iterable<int>.generate(0xA7).toList();
          sink.add([$7, $cr, $lf]..addAll(data));
          expect(results, equals([data]));
        });

        test("after chunk size", () {
          sink.add([$3]);
          expect(results, isEmpty);

          sink.add([$cr, $lf, 1, 2, 3]);
          expect(
              results,
              equals([
                [1, 2, 3]
              ]));
        });

        test("after CR", () {
          sink.add([$3, $cr]);
          expect(results, isEmpty);

          sink.add([$lf, 1, 2, 3]);
          expect(
              results,
              equals([
                [1, 2, 3]
              ]));
        });

        test("after LF", () {
          sink.add([$3, $cr, $lf]);
          expect(results, isEmpty);

          sink.add([1, 2, 3]);
          expect(
              results,
              equals([
                [1, 2, 3]
              ]));
        });

        test("after some bytes", () {
          sink.add([$3, $cr, $lf, 1, 2]);
          expect(
              results,
              equals([
                [1, 2]
              ]));

          sink.add([3]);
          expect(
              results,
              equals([
                [1, 2],
                [3]
              ]));
        });

        test("after all bytes", () {
          sink.add([$3, $cr, $lf, 1, 2, 3]);
          expect(
              results,
              equals([
                [1, 2, 3]
              ]));

          sink.add([$cr, $lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]);
          expect(
              results,
              equals([
                [1, 2, 3],
                [2, 3, 4]
              ]));
        });

        test("after a post-chunk CR", () {
          sink.add([$3, $cr, $lf, 1, 2, 3, $cr]);
          expect(
              results,
              equals([
                [1, 2, 3]
              ]));

          sink.add([$lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]);
          expect(
              results,
              equals([
                [1, 2, 3],
                [2, 3, 4]
              ]));
        });

        test("after a post-chunk LF", () {
          sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]);
          expect(
              results,
              equals([
                [1, 2, 3]
              ]));

          sink.add([$3, $cr, $lf, 2, 3, 4, $cr, $lf]);
          expect(
              results,
              equals([
                [1, 2, 3],
                [2, 3, 4]
              ]));
        });

        test("after empty chunk size", () {
          sink.add([$0]);
          expect(results, isEmpty);

          sink.add([$cr, $lf, $cr, $lf]);
          expect(results, isEmpty);

          sink.close();
          expect(results, isEmpty);
        });

        test("after first empty chunk CR", () {
          sink.add([$0, $cr]);
          expect(results, isEmpty);

          sink.add([$lf, $cr, $lf]);
          expect(results, isEmpty);

          sink.close();
          expect(results, isEmpty);
        });

        test("after first empty chunk LF", () {
          sink.add([$0, $cr, $lf]);
          expect(results, isEmpty);

          sink.add([$cr, $lf]);
          expect(results, isEmpty);

          sink.close();
          expect(results, isEmpty);
        });

        test("after second empty chunk CR", () {
          sink.add([$0, $cr, $lf, $cr]);
          expect(results, isEmpty);

          sink.add([$lf]);
          expect(results, isEmpty);

          sink.close();
          expect(results, isEmpty);
        });
      });

      group("addSlice()", () {
        test("adds bytes from the specified slice", () {
          sink.addSlice([1, $3, $cr, $lf, 2, 3, 4, 5], 1, 7, false);
          expect(
              results,
              equals([
                [2, 3, 4]
              ]));
        });

        test("doesn't decode if the slice is empty", () {
          sink.addSlice([1, 2, 3, 4, 5], 1, 1, false);
          expect(results, isEmpty);
        });

        test("closes the sink if isLast is true", () {
          sink.addSlice([1, $0, $cr, $lf, $cr, $lf, 7], 1, 6, true);
          expect(results, isEmpty);
        });

        group("disallows", () {
          test("start < 0", () {
            expect(() => sink.addSlice([1, 2, 3, 4, 5], -1, 4, false),
                throwsRangeError);
          });

          test("start > end", () {
            expect(() => sink.addSlice([1, 2, 3, 4, 5], 3, 2, false),
                throwsRangeError);
          });

          test("end > length", () {
            expect(() => sink.addSlice([1, 2, 3, 4, 5], 1, 10, false),
                throwsRangeError);
          });
        });
      });
    });
  });
}
