/// Licensed to the Apache Software Foundation (ASF) under one
/// or more contributor license agreements. See the NOTICE file
/// distributed with this work for additional information
/// regarding copyright ownership. The ASF licenses this file
/// to you under the Apache License, Version 2.0 (the
/// 'License'); you may not use this file except in compliance
/// with the License. You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing,
/// software distributed under the License is distributed on an
/// 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
/// KIND, either express or implied. See the License for the
/// specific language governing permissions and limitations
/// under the License.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:thrift/thrift.dart';
import 'package:thrift/thrift_console.dart';
import 'package:thrift_test/thrift_test.dart';
import 'package:http/io_client.dart';

const TEST_BASETYPES = 1; // 0000 0001
const TEST_STRUCTS = 2; // 0000 0010
const TEST_CONTAINERS = 4; // 0000 0100
const TEST_EXCEPTIONS = 8; // 0000 1000
const TEST_UNKNOWN = 64; // 0100 0000 (Failed to prepare environemt etc.)
const TEST_TIMEOUT = 128; // 1000 0000
const TEST_NOTUSED = 48; // 0011 0000 (reserved bits)

typedef Future FutureFunction();

class TTest {
  final int errorCode;
  final String name;
  final FutureFunction func;

  TTest(this.errorCode, this.name, this.func);
}

class TTestError extends Error {
  final actual;
  final expected;

  TTestError(this.actual, this.expected);

  String toString() => '$actual != $expected';
}

late List<TTest> _tests;
late ThriftTestClient client;
late bool verbose;

/// Adapted from TestClient.php
main(List<String> args) async {
  ArgResults? results = _parseArgs(args);

  if (results == null) {
    exit(TEST_UNKNOWN);
  }

  verbose = results['verbose'] == true;

  await _initTestClient(
      host: results['host'],
      port: int.parse(results['port']),
      transportType: results['transport'],
      protocolType: results['protocol']).catchError((e) {
    stdout.writeln('Error:');
    stdout.writeln('$e');
    if (e is Error) {
      stdout.writeln('${e.stackTrace}');
    }
    exit(TEST_UNKNOWN);
  });

  // run tests
  _tests = _createTests();

  int result = 0;

  for (TTest test in _tests) {
    if (verbose) stdout.write('${test.name}... ');
    try {
      await test.func();
      if (verbose) stdout.writeln('success!');
    } catch (e) {
      if (verbose) stdout.writeln('$e');
      result = result | test.errorCode;
    }
  }

  exit(result);
}

ArgResults? _parseArgs(List<String> args) {
  var parser = new ArgParser();
  parser.addOption('host', defaultsTo: 'localhost', help: 'The server host');
  parser.addOption('port', defaultsTo: '9090', help: 'The port to connect to');
  parser.addOption('transport',
      defaultsTo: 'buffered',
      allowed: ['buffered', 'framed', 'http'],
      help: 'The transport name',
      allowedHelp: {
        'buffered': 'TBufferedTransport',
        'framed': 'TFramedTransport'
      });
  parser.addOption('protocol',
      defaultsTo: 'binary',
      allowed: ['binary', 'compact', 'json'],
      help: 'The protocol name',
      allowedHelp: {
        'binary': 'TBinaryProtocol',
        'compact': 'TCompactProtocol',
        'json': 'TJsonProtocol'
      });
  parser.addFlag('verbose', defaultsTo: true);

  ArgResults? results;
  try {
    results = parser.parse(args);
  } catch (e) {
    stdout.writeln('$e\n');
  }

  if (results == null) stdout.write(parser.usage);

  return results;
}

TProtocolFactory getProtocolFactory(String protocolType) {
  if (protocolType == 'binary') {
    return new TBinaryProtocolFactory();
  } else if (protocolType == 'compact') {
    return new TCompactProtocolFactory();
  } else if (protocolType == 'json') {
    return new TJsonProtocolFactory();
  }

  throw new ArgumentError.value(protocolType);
}

Future _initTestClient(
    {required String host, required int port, required String transportType, required String protocolType}) async {
  TTransport transport;
  var protocolFactory = getProtocolFactory(protocolType);

  if (transportType == 'http') {
    var httpClient = IOClient();
    var uri = Uri.parse('http://$host:$port');
    var config = new THttpConfig(uri, {});
    transport = new THttpClientTransport(httpClient, config);
  } else {
    var socket = await Socket.connect(host, port);
    transport = new TClientSocketTransport(new TTcpSocket(socket));
    if (transportType == 'framed') {
      transport = new TFramedTransport(transport);
    }
  }

  var protocol = protocolFactory.getProtocol(transport);
  client = new ThriftTestClient(protocol);

  await transport.open();
}

List<TTest> _createTests() {
  List<TTest> tests = [];

  var xtruct = new Xtruct()
    ..string_thing = 'Zero'
    ..byte_thing = 1
    ..i32_thing = -3
    ..i64_thing = -5;

  tests.add(new TTest(TEST_BASETYPES, 'testVoid', () async {
    await client.testVoid();
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testString', () async {
    var input = 'Test';
    var result = await client.testString(input);
    if (result != input) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testBool', () async {
    var input = true;
    var result = await client.testBool(input);
    if (result != input) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testByte', () async {
    var input = 64;
    var result = await client.testByte(input);
    if (result != input) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testI32', () async {
    var input = 2147483647;
    var result = await client.testI32(input);
    if (result != input) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testI64', () async {
    var input = 9223372036854775807;
    var result = await client.testI64(input);
    if (result != input) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testDouble', () async {
    var input = 3.1415926;
    var result = await client.testDouble(input);
    if (result != input) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testBinary', () async {
    var utf8Codec = const Utf8Codec();
    var input = utf8Codec.encode('foo');
    var result = await client.testBinary(input);
    var equality = const ListEquality();
    if (!equality.equals(result, input)) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testStruct', () async {
    var result = await client.testStruct(xtruct);
    if ('$result' != '$xtruct') throw new TTestError(result, xtruct);
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testNest', () async {
    var input = new Xtruct2()
      ..byte_thing = 1
      ..struct_thing = xtruct
      ..i32_thing = -3;

    var result = await client.testNest(input);
    if ('$result' != '$input') throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testMap', () async {
    Map<int, int> input = {1: -10, 2: -9, 3: -8, 4: -7, 5: -6};

    var result = await client.testMap(input);
    var equality = const MapEquality();
    if (!equality.equals(result, input)) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testSet', () async {
    var input = new Set<int>.from([-2, -1, 0, 1, 2]);
    var result = await client.testSet(input);
    var equality = const SetEquality();
    if (!equality.equals(result, input)) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testList', () async {
    var input = [-2, -1, 0, 1, 2];
    var result = await client.testList(input);
    var equality = const ListEquality();
    if (!equality.equals(result, input)) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testEnum', () async {
    await _testEnum(Numberz.ONE);
    await _testEnum(Numberz.TWO);
    await _testEnum(Numberz.THREE);
    await _testEnum(Numberz.FIVE);
    await _testEnum(Numberz.EIGHT);
  }));

  tests.add(new TTest(TEST_BASETYPES, 'testTypedef', () async {
    var input = 309858235082523;
    var result = await client.testTypedef(input);
    if (result != input) throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testMapMap', () async {
    Map<int, Map<int, int>>? result = await client.testMapMap(1);
    if (result != null && (result.isEmpty || (result[result.keys.first] != null && result[result.keys.first]!.isEmpty))) {
      throw new TTestError(result, 'Map<int, Map<int, int>>');
    }
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testInsanity', () async {
    var input = new Insanity();
    input.userMap = {Numberz.FIVE: 5000};
    input.xtructs = [xtruct];

    Map<int, Map<int, Insanity>>? result = await client.testInsanity(input);
    if (result != null &&  ( result.isEmpty || (result[result.keys.first] != null && result[result.keys.first]!.isEmpty))) {
      throw new TTestError(result, 'Map<int, Map<int, Insanity>>');
    }
  }));

  tests.add(new TTest(TEST_CONTAINERS, 'testMulti', () async {
    var input = new Xtruct()
      ..string_thing = 'Hello2'
      ..byte_thing = 123
      ..i32_thing = 456
      ..i64_thing = 789;

    var result = await client.testMulti(input.byte_thing, input.i32_thing,
        input.i64_thing, {1: 'one'}, Numberz.EIGHT, 5678);
    if ('$result' != '$input') throw new TTestError(result, input);
  }));

  tests.add(new TTest(TEST_EXCEPTIONS, 'testException', () async {
    try {
      await client.testException('Xception');
    } on Xception catch (_) {
      return;
    }

    throw new TTestError(null, 'Xception');
  }));

  tests.add(new TTest(TEST_EXCEPTIONS, 'testMultiException', () async {
    try {
      await client.testMultiException('Xception2', 'foo');
    } on Xception2 catch (_) {
      return;
    }

    throw new TTestError(null, 'Xception2');
  }));

  return tests;
}

Future _testEnum(int input) async {
  var result = await client.testEnum(input);
  if (result != input) throw new TTestError(result, input);
}
