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

// ignore_for_file: no_leading_underscores_for_local_identifiers

import 'dart:isolate';
import 'dart:typed_data';

import 'package:hashlib/random.dart';
import 'package:hashlib/src/random/generators.dart';
import 'package:test/test.dart';

const int _maxInt = 0xFFFFFFFF;

@pragma('vm:entry-point')
void nextSeedIsolate(SendPort port) {
  port.send($generateSeed());
}

var testRandom = HashlibRandom.custom(() => _maxInt);

void runFunctionalText(HashlibRandom rand) {
  rand.nextInt();
  rand.nextBetween(30, 50);
  rand.nextBool();
  rand.nextByte();
  rand.nextBytes(10);
  rand.nextDouble();
  rand.nextInt();
  rand.nextNumbers(10);
  rand.nextString(10);
  rand.nextWord();
}

void main() {
  group('functional tests', () {
    test("secure random", () {
      runFunctionalText(HashlibRandom(RNG.secure));
    });
    test("system random", () {
      runFunctionalText(HashlibRandom(RNG.system));
    });
    group("keccak random", () {
      test('functions', () {
        runFunctionalText(HashlibRandom(RNG.keccak));
      });
      test('with seed', () {
        var rand = HashlibRandom(RNG.keccak, seed: 100);
        expect(rand.nextInt(), 3662713900);
      });
    });
    group("sha256 random", () {
      test("functions", () {
        runFunctionalText(HashlibRandom(RNG.sha256));
      });
      test('with seed', () {
        var rand = HashlibRandom(RNG.sha256, seed: 100);
        expect(rand.nextInt(), 3624764737);
      });
    });
    group("sm3 random", () {
      test("functions", () {
        runFunctionalText(HashlibRandom(RNG.sm3));
      });
      test('with seed', () {
        var rand = HashlibRandom(RNG.sm3, seed: 100);
        expect(rand.nextInt(), 874203019);
      });
    });
    group("md5 random", () {
      test("functions", () {
        runFunctionalText(HashlibRandom(RNG.md5));
      });
      test('with seed', () {
        var rand = HashlibRandom(RNG.md5, seed: 100);
        expect(rand.nextInt(), 368434865);
      });
    });
    test("xxh64 random", () {
      runFunctionalText(HashlibRandom(RNG.xxh64));
    }, tags: ['vm-only']);
  });

  test('seed generator uniqueness with futures', () async {
    final seeds = await Future.wait(List.generate(
      1000,
      (_) => Future.microtask($generateSeed),
    ));
    expect(seeds.toSet().length, 1000);
  });

  test('seed generator uniqueness with isolates', () async {
    final receiver = ReceivePort();
    await Future.wait(List.generate(
      100,
      (_) => Isolate.spawn(nextSeedIsolate, receiver.sendPort),
    ));
    final seeds = await receiver.take(100).toList();
    expect(seeds.toSet().length, 100);
  }, tags: ['vm-only'], timeout: Timeout(Duration(minutes: 5)));

  test('random bytes length = 0', () {
    expect(randomBytes(0), []);
  });
  test('random bytes length = 1', () {
    expect(randomBytes(1).length, 1);
  });
  test('random bytes length = 100', () {
    expect(randomBytes(100).length, 100);
  });

  test('random numbers length = 0', () {
    expect(randomNumbers(0), []);
  });
  test('random numbers length = 1', () {
    expect(randomNumbers(1).length, 1);
  });
  test('random numbers length = 100', () {
    expect(randomNumbers(100).length, 100);
  });
  test('random numbers value', () {
    final result = randomNumbers(10);
    expect(result, anyElement(greaterThan(255)));
  });

  test('fill random bytes', () {
    var data = Uint8List(10);
    fillRandom(data.buffer);
    expect(data, anyElement(isNonZero));
  });

  test('fill random numbers', () {
    var data = Uint32List(10);
    fillNumbers(data);
    expect(data, anyElement(greaterThan(255)));
  });

  test('fill test random', () {
    int i, c;
    for (c = 0; c <= 100; ++c) {
      for (i = 0; i + c <= 100; ++i) {
        var data = Uint8List(100);
        testRandom.fill(data.buffer, i, c);
        int s = data.fold<int>(0, (p, e) => p + (e > 0 ? 1 : 0));
        expect(s, c, reason: 'fill($i, $c) : $data');
      }
    }
  });

  test('next between', () {
    var rand = HashlibRandom.secure();
    expect(rand.nextBetween(0, 0), 0);
    expect(rand.nextBetween(1, 1), 1);
    expect(rand.nextBetween(5, 10), lessThanOrEqualTo(10));
    expect(rand.nextBetween(10, 5), greaterThanOrEqualTo(5));
    expect(rand.nextBetween(-5, -2), lessThan(0));
    expect(rand.nextBetween(-5, -15), lessThan(0));
    for (int i = 0; i < 100; ++i) {
      expect(rand.nextBetween(0, 1), lessThanOrEqualTo(1));
      expect(rand.nextBetween(0, 3), lessThanOrEqualTo(3));
      expect(rand.nextBetween(0, 10), lessThanOrEqualTo(10));
      expect(rand.nextBetween(0, 50), lessThanOrEqualTo(50));
      expect(rand.nextBetween(0, 500), lessThanOrEqualTo(500));
      expect(rand.nextBetween(0, 85701), lessThanOrEqualTo(85701));
      expect(rand.nextBetween(1, _maxInt), greaterThanOrEqualTo(1));
      expect(rand.nextBetween(3, _maxInt), greaterThanOrEqualTo(3));
      expect(rand.nextBetween(10, _maxInt), greaterThanOrEqualTo(10));
      expect(rand.nextBetween(50, _maxInt), greaterThanOrEqualTo(50));
      expect(rand.nextBetween(500, _maxInt), greaterThanOrEqualTo(500));
      expect(rand.nextBetween(85701, _maxInt), greaterThanOrEqualTo(85701));
    }
  });

  test('random string throws StateError on empty whitelist', () {
    expect(
        () => randomString(
              50,
              whitelist: [],
            ),
        throwsStateError);
    expect(
        () => randomString(
              50,
              whitelist: [1, 2, 3],
              blacklist: [1, 2, 3],
            ),
        throwsStateError);
    expect(
        () => randomString(
              50,
              numeric: true,
              blacklist: '0123456789'.codeUnits,
            ),
        throwsStateError);
  });

  group('HashlibRandom.nextString', () {
    late HashlibRandom random;
    final _lower = 'abcdefghijklmnopqrstuvwxyz'.codeUnits;
    final _upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.codeUnits;
    final _digits = '0123456789'.codeUnits;
    final _controls = [
      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
      11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
      21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
      127
    ];
    final _punctuations = [
      33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, //
      58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 95, 96, 123, 124, 125, 126,
    ];

    setUp(() {
      // Mock generator with predictable values
      final items = [
        65, 66, 67, 68, 69, 70, 71, 72, 73, 74, //
        75, 76, 77, 78, 79, 80, 81, 82, 83, 84
      ];
      int p = 0;
      random = HashlibRandom.custom(() {
        p %= items.length;
        return items[p++];
      });
    });

    test('should return a string of correct length', () {
      final result = random.nextString(10);
      expect(result.length, equals(10));
    });

    test('should contain only ASCII characters by default', () {
      final result = random.nextString(10);
      for (int i = 0; i < result.length; i++) {
        expect(result.codeUnitAt(i), inInclusiveRange(0, 127));
      }
    });

    test('should include only lowercase characters when lower is true', () {
      final result = random.nextString(
        25,
        lower: true,
        upper: false,
        numeric: false,
        controls: false,
        punctuations: false,
      );
      expect(result.codeUnits, everyElement(isIn(_lower)));
    });

    test('should include uppercase characters when upper is true', () {
      final result = random.nextString(
        25,
        lower: false,
        upper: true,
        numeric: false,
        controls: false,
        punctuations: false,
      );
      expect(result.codeUnits, everyElement(isIn(_upper)));
    });

    test('should include numeric characters when numeric is true', () {
      final result = random.nextString(
        25,
        lower: false,
        upper: false,
        numeric: true,
        controls: false,
        punctuations: false,
      );
      expect(result.codeUnits, everyElement(isIn(_digits)));
    });

    test('should include control characters when controls is true', () {
      final result = random.nextString(
        25,
        lower: false,
        upper: false,
        numeric: false,
        controls: true,
        punctuations: false,
      );
      expect(result.codeUnits, everyElement(isIn(_controls)));
    });

    test('should include punctuation characters when punctuations is true', () {
      final result = random.nextString(
        25,
        lower: false,
        upper: false,
        numeric: false,
        controls: false,
        punctuations: true,
      );
      expect(result.codeUnits, everyElement(isIn(_punctuations)));
    });

    test('should include multiple (lower, numeric)', () {
      final result = random.nextString(
        50,
        lower: true,
        upper: false,
        numeric: true,
        controls: false,
        punctuations: false,
      );
      final matcher = [..._lower, ..._digits];
      expect(result.codeUnits, everyElement(isIn(matcher)));
    });

    test('should include multiple (upper, numeric)', () {
      final result = random.nextString(
        50,
        lower: false,
        upper: true,
        numeric: true,
        controls: false,
        punctuations: false,
      );
      final matcher = [..._upper, ..._digits];
      expect(result.codeUnits, everyElement(isIn(matcher)));
    });

    test('should include multiple (numeric, controls)', () {
      final result = random.nextString(
        50,
        lower: false,
        upper: false,
        numeric: true,
        controls: true,
        punctuations: false,
      );
      final matcher = [..._controls, ..._digits];
      expect(result.codeUnits, everyElement(isIn(matcher)));
    });

    test('should include multiple (lower, punctuations)', () {
      final result = random.nextString(
        50,
        lower: true,
        upper: false,
        numeric: false,
        controls: false,
        punctuations: true,
      );
      final matcher = [..._lower, ..._punctuations];
      expect(result.codeUnits, everyElement(isIn(matcher)));
    });

    test('should use whitelist if provided', () {
      final whitelist = [65, 66, 67]; // A, B, C
      final result = random.nextString(10, whitelist: whitelist);
      expect(result.codeUnits, everyElement(isIn(whitelist)));
    });

    test('should remove characters in blacklist', () {
      final blacklist = [65, 66, 67]; // A, B, C
      final result = random.nextString(10, blacklist: blacklist, lower: true);
      expect(result.codeUnits, isNot(anyOf(blacklist)));
    });

    test('should throw StateError if whitelist is empty', () {
      expect(() => random.nextString(10, whitelist: []),
          throwsA(isA<StateError>()));
    });

    test('should return an empty string if length is 0', () {
      final result = random.nextString(0);
      expect(result, isEmpty);
    });

    test('should return deterministic output with the same seed', () {
      int p = 0, q = 0;
      final random1 = HashlibRandom.custom(() => p++);
      final random2 = HashlibRandom.custom(() => q++);

      final result1 = random1.nextString(10);
      final result2 = random2.nextString(10);

      expect(result1, equals(result2));
    });
  });

  group('Test \$seedList', () {
    test('Test with a normal length list', () {
      int seed = 123456789;
      var data = Uint8List(64);
      $seedList(data, seed);
      expect(data, isNot(equals(Uint8List(64))));
    });

    test('Test with small list', () {
      int seed = 123456789;
      for (int i = 1; i < 8; ++i) {
        var data = Uint8List(i);
        $seedList(data, seed);
        expect(data, isNot(equals(Uint8List(i))));
      }
    });

    test('Test with uneven list', () {
      int seed = 123456789;
      for (int i = 1; i < 4; ++i) {
        var data = Uint8List(64 + i);
        $seedList(data, seed);
        expect(data.skip(64), isNot(equals(Uint8List(i))));
      }
    });

    test('Test with same seed', () {
      int seed = 123456789;
      var data1 = Uint8List(255);
      var data2 = Uint8List(255);
      $seedList(data1, seed);
      $seedList(data2, seed);
      expect(data1, equals(data2));
    });

    test('Test with different seed', () {
      int seed1 = 123456789;
      int seed2 = 987654321;
      var data1 = Uint8List(255);
      var data2 = Uint8List(255);
      $seedList(data1, seed1);
      $seedList(data2, seed2);
      expect(data1, isNot(equals(data2)));
    });
  });
}
