import 'dart:io';
import 'dart:typed_data';

import 'package:minio/io.dart';
import 'package:minio/minio.dart';
import 'package:minio/src/minio_models_generated.dart';
import 'package:minio/src/utils.dart';
import 'package:test/test.dart';

import 'helpers.dart';

void main() {
  testConstruct();
  testListBuckets();
  testBucketExists();
  testFPutObject();
  testGetObjectACL();
  testSetObjectACL();
  testGetObject();
  testPutObject();
  testGetBucketNotification();
  testSetBucketNotification();
  testRemoveAllBucketNotification();
  testListenBucketNotification();
  testStatObject();
  testMakeBucket();
  testRemoveBucket();
  testRemoveObject();
  testListObjects();
}

void testConstruct() {
  test('Minio() implies http port', () {
    final client = getMinioClient(port: null, useSSL: false);
    expect(client.port, equals(80));
  });

  test('Minio() implies https port', () {
    final client = getMinioClient(port: null, useSSL: true);
    expect(client.port, equals(443));
  });

  test('Minio() overrides port with http', () {
    final client = getMinioClient(port: 1234, useSSL: false);
    expect(client.port, equals(1234));
  });

  test('Minio() overrides port with https', () {
    final client = getMinioClient(port: 1234, useSSL: true);
    expect(client.port, equals(1234));
  });

  test('Minio() throws when endPoint is url', () {
    expect(
      () => getMinioClient(endpoint: 'http://play.min.io'),
      throwsA(isA<MinioError>()),
    );
  });

  test('Minio() throws when port is invalid', () {
    expect(
      () => getMinioClient(port: -1),
      throwsA(isA<MinioError>()),
    );

    expect(
      () => getMinioClient(port: 65536),
      throwsA(isA<MinioError>()),
    );
  });
}

void testListBuckets() {
  test('listBuckets() succeeds', () async {
    final minio = getMinioClient();

    expect(() async => await minio.listBuckets(), returnsNormally);
  });

  test('listBuckets() can list buckets', () async {
    final minio = getMinioClient();
    final bucketName1 = uniqueName();
    await minio.makeBucket(bucketName1);

    final bucketName2 = uniqueName();
    await minio.makeBucket(bucketName2);

    final buckets = await minio.listBuckets();
    expect(buckets.any((b) => b.name == bucketName1), isTrue);
    expect(buckets.any((b) => b.name == bucketName2), isTrue);

    await minio.removeBucket(bucketName1);
    await minio.removeBucket(bucketName2);
  });

  test('listBuckets() fails due to wrong access key', () async {
    final minio = getMinioClient(accessKey: 'incorrect-access-key');

    expect(
      () async => await minio.listBuckets(),
      throwsA(
        isA<MinioS3Error>().having(
          (e) => e.error!.code,
          'code',
          isIn(['AccessDenied', 'InvalidAccessKeyId']),
        ),
      ),
    );
  });

  test('listBuckets() fails due to wrong secret key', () async {
    final minio = getMinioClient(secretKey: 'incorrect-secret-key');

    expect(
      () async => await minio.listBuckets(),
      throwsA(
        isA<MinioS3Error>().having(
          (e) => e.error!.code,
          'code',
          isIn(['AccessDenied', 'SignatureDoesNotMatch']),
        ),
      ),
    );
  });
}

void testBucketExists() {
  group('bucketExists', () {
    final bucketName = uniqueName();

    setUpAll(() async {
      final minio = getMinioClient();
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      final minio = getMinioClient();
      await minio.removeBucket(bucketName);
    });

    test('bucketExists() returns true for an existing bucket', () async {
      final minio = getMinioClient();
      expect(await minio.bucketExists(bucketName), equals(true));
    });

    test('bucketExists() returns false for a non-existent bucket', () async {
      final minio = getMinioClient();
      expect(
          await minio.bucketExists('non-existing-bucket-name'), equals(false));
    });

    test('bucketExists() fails due to wrong access key', () async {
      final minio = getMinioClient(accessKey: 'incorrect-access-key');
      expect(
        () async => await minio.bucketExists(bucketName),
        throwsA(
          isA<MinioError>().having(
            (e) => e.message,
            'message',
            'Forbidden',
          ),
        ),
      );
    });

    test('bucketExists() fails due to wrong secret key', () async {
      final minio = getMinioClient(secretKey: 'incorrect-secret-key');
      expect(
        () async => await minio.bucketExists(bucketName),
        throwsA(
          isA<MinioError>().having(
            (e) => e.message,
            'message',
            'Forbidden',
          ),
        ),
      );
    });
  });
}

void testFPutObject() {
  group('fPutObject', () {
    final bucketName = uniqueName();
    late Directory tempDir;
    late File testFile;
    final objectName = 'a.jpg';

    setUpAll(() async {
      tempDir = await Directory.systemTemp.createTemp();
      testFile = await File('${tempDir.path}/$objectName').create();
      await testFile.writeAsString('random bytes');

      final minio = getMinioClient();
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      final minio = getMinioClient();
      await minio.removeObject(bucketName, objectName);
      await tempDir.delete(recursive: true);
    });

    test('fPutObject() inserts content-type to metadata', () async {
      final minio = getMinioClient();
      await minio.fPutObject(bucketName, objectName, testFile.path);

      final stat = await minio.statObject(bucketName, objectName);
      expect(stat.metaData!['content-type'], equals('image/jpeg'));
    });

    test('fPutObject() adds user-defined object metadata w/ prefix', () async {
      final prefix = 'x-amz-meta-';
      final userDefinedMetadataKey = '${prefix}user-defined-metadata-key-1';
      final userDefinedMetadataValue = 'custom value 1';
      final metadata = {
        userDefinedMetadataKey: userDefinedMetadataValue,
      };

      final minio = getMinioClient();
      await minio.fPutObject(bucketName, objectName, testFile.path, metadata);

      final stat = await minio.statObject(bucketName, objectName);
      expect(
        stat.metaData![userDefinedMetadataKey.substring(prefix.length)],
        equals(userDefinedMetadataValue),
      );
    });

    test('fPutObject() adds user-defined object metadata w/o prefix', () async {
      final userDefinedMetadataKey = 'user-defined-metadata-key-2';
      final userDefinedMetadataValue = 'custom value 2';
      final metadata = {
        userDefinedMetadataKey: userDefinedMetadataValue,
      };

      final minio = getMinioClient();
      await minio.fPutObject(bucketName, objectName, testFile.path, metadata);

      final stat = await minio.statObject(bucketName, objectName);
      expect(stat.metaData![userDefinedMetadataKey],
          equals(userDefinedMetadataValue));
    });

    test('fPutObject() with empty file', () async {
      final objectName = 'empty.txt';
      final emptyFile = await File('${tempDir.path}/$objectName').create();
      await emptyFile.writeAsString('');

      final minio = getMinioClient();
      await minio.fPutObject(bucketName, objectName, emptyFile.path);

      final stat = await minio.statObject(bucketName, objectName);
      expect(stat.size, equals(0));
    });
  });
}

void testSetObjectACL() {
  group('setObjectACL', () {
    late String bucketName;
    late Directory tempDir;
    File testFile;
    final objectName = 'a.jpg';

    setUpAll(() async {
      bucketName = uniqueName();

      tempDir = await Directory.systemTemp.createTemp();
      testFile = await File('${tempDir.path}/$objectName').create();
      await testFile.writeAsString('random bytes');

      final minio = getMinioClient();
      await minio.makeBucket(bucketName);

      await minio.fPutObject(bucketName, objectName, testFile.path);
    });

    tearDownAll(() async {
      await tempDir.delete(recursive: true);
    });

    test('setObjectACL() set objects acl', () async {
      final minio = getMinioClient();
      await minio.setObjectACL(bucketName, objectName, 'public-read');
    });
  });
}

void testGetObjectACL() {
  group('getObjectACL', () {
    late String bucketName;
    late Directory tempDir;
    File testFile;
    final objectName = 'a.jpg';

    setUpAll(() async {
      bucketName = uniqueName();

      tempDir = await Directory.systemTemp.createTemp();
      testFile = await File('${tempDir.path}/$objectName').create();
      await testFile.writeAsString('random bytes');

      final minio = getMinioClient();
      await minio.makeBucket(bucketName);

      await minio.fPutObject(bucketName, objectName, testFile.path);
    });

    tearDownAll(() async {
      await tempDir.delete(recursive: true);
    });

    test('getObjectACL() fetch objects acl', () async {
      final minio = getMinioClient();
      var acl = await minio.getObjectACL(bucketName, objectName);
      expect(acl.grants!.permission, equals(null));
    });
  });
}

void testGetObject() {
  group('getObject()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();
    final object = uniqueName();
    final objectUtf8 = uniqueName() + '/あるところ/某个文件.🦐';
    final objectData = Uint8List.fromList([1, 2, 3]);

    setUpAll(() async {
      await minio.makeBucket(bucketName);
      await minio.putObject(bucketName, object, Stream.value(objectData));
      await minio.putObject(bucketName, objectUtf8, Stream.value(objectData));
    });

    tearDownAll(() async {
      await minio.removeObject(bucketName, object);
      await minio.removeObject(bucketName, objectUtf8);
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      final stream = await minio.getObject(bucketName, object);
      final buffer = BytesBuilder();
      await stream.forEach((data) => buffer.add(data));
      expect(stream.contentLength, equals(objectData.length));
      expect(buffer.takeBytes(), equals(objectData));
    });

    test('succeeds with utf8 object name', () async {
      final stream = await minio.getObject(bucketName, object);
      final buffer = BytesBuilder();
      await stream.forEach((data) => buffer.add(data));
      expect(stream.contentLength, equals(objectData.length));
      expect(buffer.takeBytes(), equals(objectData));
    });

    test('fails on invalid bucket', () {
      expect(
        () async => await minio.getObject('$bucketName-invalid', object),
        throwsA(isA<MinioError>()),
      );
    });

    test('fails on invalid object', () {
      expect(
        () async => await minio.getObject(bucketName, '$object-invalid'),
        throwsA(isA<MinioError>()),
      );
    });
  });
}

void testPutObject() {
  group('putObject()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();
    final objectData = Uint8List.fromList([1, 2, 3]);

    setUpAll(() async {
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      final objectName = uniqueName();
      await minio.putObject(bucketName, objectName, Stream.value(objectData));
      final stat = await minio.statObject(bucketName, objectName);
      expect(stat.size, equals(objectData.length));
      await minio.removeObject(bucketName, objectName);
    });

    test('works with object names with symbols', () async {
      final objectName = uniqueName() + r'-._~,!@#$%^&*()';
      await minio.putObject(bucketName, objectName, Stream.value(objectData));
      final stat = await minio.statObject(bucketName, objectName);
      expect(stat.size, equals(objectData.length));
      await minio.removeObject(bucketName, objectName);
    });

    test('progress report works', () async {
      final objectName = uniqueName();
      int? progress;
      await minio.putObject(
        bucketName,
        objectName,
        Stream.value(objectData),
        onProgress: (bytes) => progress = bytes,
      );
      await minio.removeObject(bucketName, objectName);
      expect(progress, equals(objectData.length));
    });

    test('medium size file upload works', () async {
      final objectName = uniqueName();
      final dataLength = 1024 * 1024;
      final data = Uint8List.fromList(List<int>.generate(dataLength, (i) => i));
      await minio.putObject(bucketName, objectName, Stream.value(data));
      final stat = await minio.statObject(bucketName, objectName);
      await minio.removeObject(bucketName, objectName);
      expect(stat.size, equals(dataLength));
    });

    test('stream upload works', () async {
      final objectName = uniqueName();
      final dataLength = 1024 * 1024;
      final data = Uint8List.fromList(List<int>.generate(dataLength, (i) => i));
      await minio.putObject(
        bucketName,
        objectName,
        Stream.value(data).transform(MaxChunkSize(123)),
      );
      final stat = await minio.statObject(bucketName, objectName);
      await minio.removeObject(bucketName, objectName);
      expect(stat.size, equals(dataLength));
    });

    test('empty stream upload works', () async {
      final objectName = uniqueName();
      await minio.putObject(bucketName, objectName, Stream.empty());
      final stat = await minio.statObject(bucketName, objectName);
      await minio.removeObject(bucketName, objectName);
      expect(stat.size, equals(0));
    });

    test('zero byte stream upload works', () async {
      final objectName = uniqueName();
      await minio.putObject(bucketName, objectName, Stream.value(Uint8List(0)));
      final stat = await minio.statObject(bucketName, objectName);
      await minio.removeObject(bucketName, objectName);
      expect(stat.size, equals(0));
    });

    test('multipart file upload works', () async {
      final objectName = uniqueName();
      final dataLength = 12 * 1024 * 1024;
      final data = Uint8List.fromList(List<int>.generate(dataLength, (i) => i));
      await minio.putObject(
        bucketName,
        objectName,
        Stream.value(data),
        chunkSize: 5 * 1024 * 1024,
      );
      final stat = await minio.statObject(bucketName, objectName);
      await minio.removeObject(bucketName, objectName);
      expect(stat.size, equals(dataLength));
    });
  });
}

void testGetBucketNotification() {
  group('getBucketNotification()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();

    setUpAll(() async {
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      await minio.getBucketNotification(bucketName);
    });
  });
}

void testSetBucketNotification() {
  group('setBucketNotification()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();

    setUpAll(() async {
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      await minio.setBucketNotification(
        bucketName,
        NotificationConfiguration(null, null, null),
      );
    });
  });
}

void testRemoveAllBucketNotification() {
  group('removeAllBucketNotification()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();

    setUpAll(() async {
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      await minio.removeAllBucketNotification(bucketName);
    });
  });
}

void testListenBucketNotification() {
  group('listenBucketNotification()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();
    // final objectName = uniqueName();

    setUpAll(() async {
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      final poller = minio.listenBucketNotification(bucketName);
      expect(poller.isStarted, isTrue);
      poller.stop();
    });

    // test('can receive notification', () async {
    //   final poller = minio.listenBucketNotification(
    //     bucketName,
    //     events: ['s3:ObjectCreated:*'],
    //   );

    //   final receivedEvents = [];
    //   poller.stream.listen((event) => receivedEvents.add(event));
    //   expect(receivedEvents, isEmpty);

    //   await minio.putObject(bucketName, objectName, Stream.value([0]));
    //   await minio.removeObject(bucketName, objectName);

    //   // FIXME: Needs sleep here
    //   expect(receivedEvents, isNotEmpty);

    //   poller.stop();
    // });
  });
}

void testStatObject() {
  group('statObject()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();
    final object = uniqueName();
    final objectUtf8 = uniqueName() + 'オブジェクト。📦';
    final data = Uint8List.fromList([1, 2, 3, 4, 5]);

    setUpAll(() async {
      await minio.makeBucket(bucketName);
      await minio.putObject(bucketName, object, Stream.value(data));
      await minio.putObject(bucketName, objectUtf8, Stream.value(data));
    });

    tearDownAll(() async {
      await minio.removeObject(bucketName, object);
      await minio.removeObject(bucketName, objectUtf8);
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      final stats = await minio.statObject(bucketName, object);
      expect(stats.lastModified, isNotNull);
      expect(stats.lastModified!.isBefore(DateTime.now()), isTrue);
      expect(stats.size, isNotNull);
      expect(stats.size, equals(data.length));
    });

    test('succeeds with utf8 object name', () async {
      final stats = await minio.statObject(bucketName, objectUtf8);
      expect(stats.lastModified, isNotNull);
      expect(stats.lastModified!.isBefore(DateTime.now()), isTrue);
      expect(stats.size, isNotNull);
      expect(stats.size, equals(data.length));
    });

    test('fails on invalid bucket', () {
      expect(
        () async => await minio.statObject('$bucketName-invalid', object),
        throwsA(isA<MinioError>()),
      );
    });

    test('fails on invalid object', () {
      expect(
        () async => await minio.statObject(bucketName, '$object-invalid'),
        throwsA(isA<MinioError>()),
      );
    });
  });
}

void testMakeBucket() {
  group('makeBucket()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();

    setUpAll(() async {
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      final buckets = await minio.listBuckets();
      final bucketNames = buckets.map((b) => b.name).toList();
      expect(bucketNames, contains(bucketName));
    });
  });
}

void testRemoveBucket() {
  group('removeBucket()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();

    test('succeeds', () async {
      await minio.makeBucket(bucketName);
      await minio.removeBucket(bucketName);
    });

    test('fails on invalid bucket name', () {
      expect(
        () async => await minio.removeBucket('$bucketName-invalid'),
        throwsA(isA<MinioError>()),
      );
    });
  });
}

void testRemoveObject() {
  group('removeObject()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();
    final objectName = uniqueName();
    final data = Uint8List.fromList([1, 2, 3, 4, 5]);

    setUpAll(() async {
      await minio.makeBucket(bucketName);
    });

    tearDownAll(() async {
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      await minio.putObject(bucketName, objectName, Stream.value(data));
      await minio.removeObject(bucketName, objectName);

      await for (var chunk in minio.listObjects(bucketName)) {
        expect(chunk.objects.contains(objectName), isFalse);
      }
    });

    test('fails on invalid bucket', () {
      expect(
        () async => await minio.removeObject('$bucketName-invalid', objectName),
        throwsA(isA<MinioError>()),
      );
    });

    test('does not throw on invalid object', () async {
      await minio.removeObject(bucketName, '$objectName-invalid');
    });
  });
}

void testListObjects() {
  group('listAllObjects()', () {
    final minio = getMinioClient();
    final bucketName = uniqueName();
    final objectName = uniqueName();
    final objectNameUtf8 = uniqueName() + '文件ファイル。ㄴㅁㄴ';
    final data = Uint8List.fromList([1, 2, 3, 4, 5]);

    setUpAll(() async {
      await minio.makeBucket(bucketName);
      await minio.putObject(bucketName, objectName, Stream.value(data));
      await minio.putObject(bucketName, objectNameUtf8, Stream.value(data));
    });

    tearDownAll(() async {
      await minio.removeObject(bucketName, objectName);
      await minio.removeObject(bucketName, objectNameUtf8);
      await minio.removeBucket(bucketName);
    });

    test('succeeds', () async {
      final result = await minio.listAllObjects(bucketName);
      print(result);
      expect(result.objects.map((e) => e.key).contains(objectName), isTrue);
      expect(result.objects.map((e) => e.key).contains(objectNameUtf8), isTrue);
    });

    test('fails on invalid bucket', () {
      expect(
        () async => await minio.listAllObjects('$bucketName-invalid'),
        throwsA(isA<MinioError>()),
      );
    });
  });

  group('listAllObjects() works when prefix contains spaces', () {
    final minio = getMinioClient();
    final bucket = uniqueName();
    final object = 'new  folder/new file.txt';
    final data = Uint8List.fromList([1, 2, 3, 4, 5]);

    setUpAll(() async {
      await minio.makeBucket(bucket);
      await minio.putObject(bucket, object, Stream.value(data));
    });

    tearDownAll(() async {
      await minio.removeObject(bucket, object);
      await minio.removeBucket(bucket);
    });

    test('succeeds', () async {
      final result = await minio.listAllObjects(bucket, prefix: 'new  folder/');
      expect(result.objects.map((e) => e.key).contains(object), isTrue);
    });
  });

  group('listAllObjects() works when prefix contains utf-8 characters', () {
    final minio = getMinioClient();
    final bucket = uniqueName();
    final prefix = '🍎🌰🍌🍓/文件夹 1 2/';
    final object = '${prefix}new file.txt';
    final data = Uint8List.fromList([1, 2, 3, 4, 5]);

    setUpAll(() async {
      await minio.makeBucket(bucket);
      await minio.putObject(bucket, object, Stream.value(data));
    });

    tearDownAll(() async {
      await minio.removeObject(bucket, object);
      await minio.removeBucket(bucket);
    });

    test('succeeds', () async {
      final result = await minio.listAllObjects(bucket, prefix: prefix);
      print(result);
      expect(result.objects.map((e) => e.key).contains(object), isTrue);
    });
  });
}