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

import 'dart:typed_data';

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

void main() {
  group('functionality test', () {
    test("name", () {
      expect(
        Argon2(
          parallelism: 1,
          memorySizeKB: 10,
          iterations: 1,
        ).name,
        r'argon2id',
      );
      expect(
        Argon2(
          parallelism: 1,
          memorySizeKB: 10,
          iterations: 1,
          type: Argon2Type.argon2id,
        ).name,
        r'argon2id',
      );
      expect(
        Argon2(
          parallelism: 1,
          memorySizeKB: 10,
          iterations: 1,
          type: Argon2Type.argon2d,
        ).name,
        r'argon2d',
      );
      expect(
        Argon2(
          parallelism: 1,
          memorySizeKB: 10,
          iterations: 1,
          type: Argon2Type.argon2i,
        ).name,
        r'argon2i',
      );
    });
  });
  // Test cases are generated by https://argon2.online/
  group('Argon2 v19 test', () {
    test("argon2i m=16, t=2, p=1 @ out = 16", () {
      final argon2 = Argon2(
        version: Argon2Version.v13,
        type: Argon2Type.argon2i,
        hashLength: 16,
        iterations: 2,
        parallelism: 1,
        memorySizeKB: 16,
        salt: "some salt".codeUnits,
      );
      final password = 'password'.codeUnits;
      final matcher = "bb5794ea66451b8fce3a84dd02d33949";
      final encoded =
          r"$argon2i$v=19$m=16,t=2,p=1$c29tZSBzYWx0$u1eU6mZFG4/OOoTdAtM5SQ";
      var result = argon2.convert(password);
      expect(result.toString(), equals(encoded));
      expect(result.hex(), matcher);
      expect(result.encoded(), encoded);
      expect(argon2.encode(password), encoded);
    });
    test("argon2d m=16, t=2, p=1 @ out = 16", () {
      final argon2 = Argon2(
        version: Argon2Version.v13,
        type: Argon2Type.argon2d,
        hashLength: 16,
        iterations: 2,
        parallelism: 1,
        memorySizeKB: 16,
        salt: "some salt".codeUnits,
      );
      final matcher = "cf916880b91ba8a1390fff6b624baa27";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
    });
    test("argon2id m=16, t=2, p=1 @ out = 16", () {
      final argon2 = Argon2(
        version: Argon2Version.v13,
        type: Argon2Type.argon2id,
        hashLength: 16,
        iterations: 2,
        parallelism: 1,
        memorySizeKB: 16,
        salt: "some salt".codeUnits,
      );
      final matcher = "88c91661b3cea3c3853593608881f324";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
    });

    test("argon2i m=256, t=2, p=4 @ out = 32", () {
      final argon2 = Argon2(
        version: Argon2Version.v13,
        type: Argon2Type.argon2i,
        hashLength: 32,
        iterations: 2,
        parallelism: 4,
        memorySizeKB: 256,
        salt: "some salt".codeUnits,
      );
      final matcher =
          "aaef1c23ce86889c7d76f5ea214760fb66900916546cde42ebdc47914daed123";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
    });
    test("argon2d m=256, t=2, p=4 @ out = 32", () {
      final argon2 = Argon2(
        version: Argon2Version.v13,
        type: Argon2Type.argon2d,
        hashLength: 32,
        iterations: 2,
        parallelism: 4,
        memorySizeKB: 256,
        salt: "some salt".codeUnits,
      );
      final matcher =
          "c31433dbefadf7aa527bbbcc7beace0d8d70973719c6efb1c24fb21278569701";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
    });
    test("argon2id m=256, t=2, p=4 @ out = 32", () {
      final argon2 = Argon2(
        version: Argon2Version.v13,
        type: Argon2Type.argon2id,
        hashLength: 32,
        iterations: 2,
        parallelism: 4,
        memorySizeKB: 256,
        salt: "some salt".codeUnits,
      );
      final matcher =
          "c23e4a305f649971527eda884bda6b481004aedd31740460da3d43db8946786f";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
    });

    test("encoded hash instance check", () {
      final encoded =
          r"$argon2id$v=19$m=128,t=1,p=4$c29tZSBzYWx0$24VHMpaU5EkkdH5rpdnb5zeOf3Y";
      final matcher = "db8547329694e44924747e6ba5d9dbe7378e7f76";
      final argon2 = Argon2.fromEncoded(fromCrypt(encoded));
      expect(argon2.type, Argon2Type.argon2id);
      expect(argon2.version, Argon2Version.v13);
      expect(argon2.memorySizeKB, 128);
      expect(argon2.parallelism, 4);
      expect(argon2.iterations, 1);
      expect(argon2.hashLength, 20);
      expect(argon2.salt, "some salt".codeUnits);
      var result = argon2.convert("password".codeUnits);
      expect(result.hex(), matcher);
      expect(result.encoded(), encoded);
    });

    test("argon2Verify with encoded", () {
      final encoded =
          r"$argon2id$v=19$m=128,t=1,p=4$c29tZSBzYWx0$24VHMpaU5EkkdH5rpdnb5zeOf3Y";
      expect(argon2Verify(encoded, "password".codeUnits), isTrue);
    });

    test("argon2Verify without the hash", () {
      final password = "password".codeUnits;
      final encoded = r"$argon2id$v=19$m=128,t=1,p=4$c29tZSBzYWx0";
      expect(() => argon2Verify(encoded, password), throwsArgumentError);
    });

    test("argon2Verify with password", () {
      final matcher = "db8547329694e44924747e6ba5d9dbe7378e7f76";
      var result = Argon2(
        type: Argon2Type.argon2id,
        salt: 'some salt'.codeUnits,
        hashLength: 20,
        iterations: 1,
        parallelism: 4,
        memorySizeKB: 128,
      ).verify(
        fromHex(matcher),
        "password".codeUnits,
      );
      expect(result, true);
    });

    test('argon2i', () {
      final result = argon2i(
        'password'.codeUnits,
        "some salt".codeUnits,
        hashLength: 16,
        security: Argon2Security(
          'custom',
          m: 16,
          p: 1,
          t: 2,
        ),
      );
      final matcher = "bb5794ea66451b8fce3a84dd02d33949";
      final encoded =
          r"$argon2i$v=19$m=16,t=2,p=1$c29tZSBzYWx0$u1eU6mZFG4/OOoTdAtM5SQ";
      expect(result.hex(), matcher);
      expect(result.encoded(), encoded);
    });

    test('argon2d', () {
      final result = argon2d(
        'password'.codeUnits,
        "some salt".codeUnits,
        hashLength: 16,
        security: Argon2Security(
          'custom',
          m: 16,
          p: 1,
          t: 2,
        ),
      );
      final matcher = "cf916880b91ba8a1390fff6b624baa27";
      final salt = r"$argon2d$v=19$m=16,t=2,p=1$c29tZSBzYWx0";
      expect(result.hex(), matcher);
      expect(result.encoded(), startsWith(salt));
    });

    test('argon2id', () {
      final result = argon2id(
        'password'.codeUnits,
        "some salt".codeUnits,
        hashLength: 16,
        security: Argon2Security(
          'custom',
          m: 16,
          p: 1,
          t: 2,
        ),
      );
      final matcher = "88c91661b3cea3c3853593608881f324";
      final salt = r"$argon2id$v=19$m=16,t=2,p=1$c29tZSBzYWx0";
      expect(result.hex(), matcher);
      expect(result.encoded(), startsWith(salt));
    });

    test("multiple call with same instance", () {
      final argon2 = Argon2(
        version: Argon2Version.v13,
        type: Argon2Type.argon2i,
        hashLength: 16,
        iterations: 2,
        parallelism: 1,
        memorySizeKB: 16,
        salt: "some salt".codeUnits,
      );
      final matcher = "bb5794ea66451b8fce3a84dd02d33949";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
    });

    test("with personalization", () {
      final personalization = "personalization".codeUnits;
      final argon2 = Argon2.fromSecurity(
        Argon2Security.test,
        hashLength: 16,
        salt: "some salt".codeUnits,
        personalization: personalization,
      );
      final matcher = "33561b7ad59b447aa5f6e3113d2f32ca";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
      expect(argon2.personalization, equals(personalization));
    });

    test("with key", () {
      final key = "random key".codeUnits;
      final argon2 = Argon2.fromSecurity(
        Argon2Security.test,
        hashLength: 16,
        salt: "some salt".codeUnits,
        key: key,
      );
      final matcher = "4ad0d2f98a6e7f6e3e99c520d1813c07";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
      expect(argon2.key, equals(key));
    });

    test("with key and personalization", () {
      final argon2 = Argon2.fromSecurity(
        Argon2Security.test,
        hashLength: 16,
        salt: "some salt".codeUnits,
        key: "random key".codeUnits,
        personalization: "personalization".codeUnits,
      );
      final matcher = "0bd2a8c0386b4125a6f439f2f863fc35";
      expect(argon2.convert('password'.codeUnits).hex(), matcher);
    });
  });

  group('Argon2Context Tests', () {
    // Test for Argon2Context constructor
    test('Creates Argon2Context with valid parameters', () {
      var salt = List<int>.generate(16, (i) => i);
      var context = Argon2Context(
        iterations: 3,
        parallelism: 2,
        memorySizeKB: 8192,
        salt: salt,
        hashLength: 32,
      );

      expect(context.salt, equals(salt));
      expect(context.passes, equals(3));
      expect(context.lanes, equals(2));
      expect(context.memorySizeKB, equals(8192));
      expect(context.hashLength, equals(32));
    });

    // Tests for invalid parameters
    test('Throws if hashLength is too small', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          hashLength: 3,
        ),
        throwsA(isA<ArgumentError>().having(
            (e) => e.message, 'message', 'The tag length must be at least 4')),
      );
    });

    test('Throws if hashLength is too large', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          hashLength: 0x3FFFFFF + 1,
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The tag length must be at most 67108863')),
      );
    });

    test('Throws if parallelism is too small', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 0,
          memorySizeKB: 8192,
        ),
        throwsA(isA<ArgumentError>().having(
            (e) => e.message, 'message', 'The parallelism must be at least 1')),
      );
    });

    test('Throws if parallelism is too large', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 0x7FFF + 1,
          memorySizeKB: 8192,
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The parallelism must be at most 32767')),
      );
    });

    test('Throws if iterations are too few', () {
      expect(
        () => Argon2Context(
          iterations: 0,
          parallelism: 2,
          memorySizeKB: 8192,
        ),
        throwsA(isA<ArgumentError>().having(
            (e) => e.message, 'message', 'The iterations must be at least 1')),
      );
    });

    test('Throws if iterations are too many', () {
      expect(
        () => Argon2Context(
          iterations: 0x3FFFFFF + 1,
          parallelism: 2,
          memorySizeKB: 8192,
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The iterations must be at most 67108863')),
      );
    });

    test('Throws if memorySizeKB is too small for parallelism', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 15,
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The memory size must be at least 8 * parallelism')),
      );
    });

    test('Throws if memorySizeKB is too large', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 0x3FFFFFF + 1,
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The memorySizeKB must be at most 67108863')),
      );
    });

    test('Throws if salt is too short', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          salt: List<int>.generate(7, (i) => i),
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The salt must be at least 8 bytes long')),
      );
    });

    test('Throws if salt is too long', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          salt: Uint8List(0x3FFFFFF + 1),
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The salt must be at most 67108863 bytes long')),
      );
    });

    test('Throws if key is too short', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          key: [],
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The key must be at least 1 bytes long')),
      );
    });

    test('Throws if key is too long', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          key: Uint8List(0x3FFFFFF + 1),
        ),
        throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
            'The key must be at most 67108863 bytes long')),
      );
    });

    test('Throws if personalization data is too short', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          personalization: [],
        ),
        throwsA(isA<ArgumentError>()),
      );
    });

    test('Throws if personalization data is too long', () {
      expect(
        () => Argon2Context(
          iterations: 3,
          parallelism: 2,
          memorySizeKB: 8192,
          personalization: Uint8List(0x3FFFFFF + 1),
        ),
        throwsArgumentError,
      );
    });

    // Test for Argon2Context.fromEncoded factory method
    test('Creates Argon2Context from encoded string with valid parameters', () {
      var cryptData = fromCrypt(
          r'$argon2id$v=19$m=8192,t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
      var context = Argon2Context.fromEncoded(cryptData);

      expect(context.type, equals(Argon2Type.argon2id));
      expect(context.version, equals(Argon2Version.v13));
      expect(context.memorySizeKB, equals(8192));
      expect(context.passes, equals(3));
      expect(context.lanes, equals(2));
      expect(context.salt.length, equals(9));
    });

    test('Throws if encoded string has invalid type', () {
      var cryptData = fromCrypt(
          r'$argon2x$v=19$m=8192,t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
      expect(
        () => Argon2Context.fromEncoded(cryptData),
        throwsA(isA<ArgumentError>()
            .having((e) => e.message, 'message', 'Unknown type')),
      );
    });

    test('Throws if encoded string has invalid version', () {
      var cryptData = fromCrypt(
          r'$argon2i$v=99$m=8192,t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
      expect(
        () => Argon2Context.fromEncoded(cryptData),
        throwsA(isA<ArgumentError>()
            .having((e) => e.message, 'message', 'Unknown version')),
      );
    });

    test('Throws if encoded string has no parameters', () {
      var cryptData =
          fromCrypt(r'$argon2i$v=19$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
      expect(
        () => Argon2Context.fromEncoded(cryptData),
        throwsA(isA<ArgumentError>()
            .having((e) => e.message, 'message', 'No paramters')),
      );
    });

    test('Throws if encoded string is missing "m" parameter', () {
      var cryptData = fromCrypt(
          r'$argon2i$v=19$t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
      expect(
        () => Argon2Context.fromEncoded(cryptData),
        throwsA(isA<ArgumentError>()
            .having((e) => e.message, 'message', 'Missing parameter: m')),
      );
    });

    test('Throws if encoded string is missing "t" parameter', () {
      var cryptData = fromCrypt(
          r'$argon2i$v=19$m=8192,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
      expect(
        () => Argon2Context.fromEncoded(cryptData),
        throwsA(isA<ArgumentError>()
            .having((e) => e.message, 'message', 'Missing parameter: t')),
      );
    });

    test('Throws if encoded string is missing "p" parameter', () {
      var cryptData = fromCrypt(
          r'$argon2i$v=19$m=8192,t=3$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
      expect(
        () => Argon2Context.fromEncoded(cryptData),
        throwsA(isA<ArgumentError>()
            .having((e) => e.message, 'message', 'Missing parameter: p')),
      );
    });
  });
}
