// Copyright (c) 2023, Sudipto Chandra
// All rights reserved. Check LICENSE file for details.

import 'dart:async';
import 'dart:typed_data';

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

void main() {
  final secret = Uint8List.fromList([
    0x1,
    0x2,
    0x3,
    0x4,
    0x5,
    0x6,
    0x7,
    0x8,
  ]);

  group('TOTP', () {
    test('algorithm name', () {
      expect(TOTP(secret).name, 'SHA1/HMAC');
      expect(TOTP(secret, algo: md5).name, 'MD5/HMAC');
    });

    test('secret, label and issuer values', () {
      var totp = TOTP(secret, label: 'test label', issuer: 'test issuer');
      expect(totp.secret, equals(secret));
      expect(totp.label, equals('test label'));
      expect(totp.issuer, equals('test issuer'));

      var totp2 = TOTP(secret);
      expect(totp2.secret, equals(secret));
      expect(totp2.label, null);
      expect(totp2.issuer, null);
    });

    test('should generate OTP of correct length', () {
      expect(TOTP(secret).valueString().length, equals(6));
    });

    test('number of digits must be at least 4', () {
      for (int i = 0; i < 20; ++i) {
        int cb() => TOTP(secret, digits: i).valueString().length;
        if (i < 4 || i > 15) {
          expect(cb, throwsA((e) => e is AssertionError), reason: 'digits: $i');
        } else {
          expect(cb(), equals(i), reason: 'digits: $i');
        }
      }
    });

    test('should generate different OTPs over time', () {
      var totp = TOTP(secret);
      int time = 30000 - totp.currentTime % 30000;
      totp.adjustClock(time);
      int otp1 = totp.value();
      totp.adjustClock(time + 30000);
      int otp2 = totp.value();
      expect(otp1, isNot(equals(otp2)));
      totp.adjustClock(time + 1);
      int otp3 = totp.value();
      expect(otp1, equals(otp3));
      totp.adjustClock(time - 100);
      int otp4 = totp.value();
      expect(otp1, isNot(equals(otp4)));
    });

    test('should adjust the clock correctly', () {
      var totp1 = TOTP(secret);
      var totp2 = TOTP(secret);
      totp2.adjustClock(30030);

      int otp1 = totp1.value();
      int otp2 = totp2.value();

      expect(otp1, isNot(equals(otp2)));
    });

    test('stream should emit OTPs periodically', () async {
      int ms = 80;
      var totp = TOTP(secret, period: Duration(milliseconds: ms));
      totp.adjustClock(ms - totp.currentTime % ms);
      final otps = <int>{};
      var subscription = totp.stream.listen((otp) {
        otps.add(otp);
      });
      await Future.delayed(
        Duration(milliseconds: 4 * ms),
        subscription.cancel,
      );
      expect(otps.length, inInclusiveRange(2, 6));
    });

    test('stream should emit OTP string periodically', () async {
      int ms = 80;
      var totp = TOTP(secret, period: Duration(milliseconds: ms));
      totp.adjustClock(ms - totp.currentTime % ms);
      final otps = <String>{};
      var subscription = totp.streamString.listen((otp) {
        otps.add(otp);
      });
      await Future.delayed(
        Duration(milliseconds: 4 * ms),
        subscription.cancel,
      );
      expect(otps.length, inInclusiveRange(2, 6));
    });
  });
}
