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

import 'package:proto_socket/proto_socket.dart';

const _host = '127.0.0.1';
const _wsPort = 29098;
const _wssPort = 29099;
const _heartbeatIntervalSeconds = 30;
const _heartbeatWaitSeconds = 10;
const _serverObservationWindow = Duration(milliseconds: 500);
const _processTimeout = Duration(seconds: 60);
const _streamTimeout = Duration(seconds: 5);

final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path;
final _dartDir = Directory('$_repoRoot/dart').path;
final _certFile = '$_repoRoot/dart/test/certs/server.crt';
final _keyFile = '$_repoRoot/dart/test/certs/server.key';

class _ServerWsClient extends WsProtobufClient {
  _ServerWsClient(WebSocket ws)
      : super(ws, _heartbeatIntervalSeconds, _heartbeatWaitSeconds, {
          TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
        });
}

class _DartWebWsServer extends WsProtobufServer {
  final void Function(WsProtobufClient) onConnected;

  _DartWebWsServer(int port, this.onConnected)
      : super(_host, port, (ws) => _ServerWsClient(ws));

  _DartWebWsServer.secure(
      int port, SecurityContext securityContext, this.onConnected)
      : super.secure(_host, port, securityContext, (ws) => _ServerWsClient(ws));

  @override
  void onClientConnected(WsProtobufClient client) => onConnected(client);
}

Future<void> main() async {
  print(
      'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
  try {
    await _runWebSuites();
    print('PASS scenario=ws-send-push');
    print('PASS scenario=ws-request-response');
    print('PASS scenario=wss-send-push');
    print('PASS scenario=wss-request-response');
    print('PASS all dart-server/dart-web-client crosstests passed');
  } catch (error) {
    stderr.writeln('FAIL crosstest error=$error');
    exitCode = 1;
  }
}

Future<void> _runWebSuites() async {
  final wsReceived = Completer<bool>();
  final wssReceived = Completer<bool>();
  final securityContext = SecurityContext()
    ..useCertificateChain(_certFile)
    ..usePrivateKey(_keyFile);
  final wsServer = _DartWebWsServer(_wsPort, (client) {
    _configureWebClient(client, wsReceived);
  });
  final wssServer =
      _DartWebWsServer.secure(_wssPort, securityContext, (client) {
    _configureWebClient(client, wssReceived);
  });

  await wsServer.start();
  await wssServer.start();
  try {
    await _runDartBrowserTest('test/browser_ws_dart_io_test.dart');
    await _expectReceived('WS', wsReceived);
    await _expectReceived('WSS', wssReceived);
  } finally {
    await Future.wait([
      wssServer.stop(),
      wsServer.stop(),
    ]);
  }
}

void _configureWebClient(WsProtobufClient client, Completer<bool> received) {
  client.addRequestListener<TestData, TestData>((req) async {
    print('SERVER_RECEIVED index=${req.index} message=${req.message}');
    if (req.index == 101 && req.message == 'fire from dart web client') {
      if (!received.isCompleted) {
        received.complete(true);
      }
      unawaited(client.send(TestData()
        ..index = 200
        ..message = 'push from dart server'));
    }
    return TestData()
      ..index = req.index * 2
      ..message = 'echo: ${req.message}';
  });
}

Future<void> _expectReceived(String transport, Completer<bool> received) async {
  final ok = await received.future
      .timeout(_serverObservationWindow, onTimeout: () => false);
  if (!ok) {
    throw StateError(
        '$transport web send-push server did not receive expected data');
  }
}

Future<void> _runDartBrowserTest(String testPath) async {
  final process = await Process.start(
    'dart',
    ['test', '-p', 'chrome', testPath],
    workingDirectory: _dartDir,
  );

  final stdoutDone = process.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen(print)
      .asFuture<void>();

  final stderrDone = process.stderr
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen(stderr.writeln)
      .asFuture<void>();

  final code = await process.exitCode.timeout(_processTimeout, onTimeout: () {
    process.kill(ProcessSignal.sigkill);
    return -1;
  });
  await Future.wait([stdoutDone, stderrDone]).timeout(_streamTimeout,
      onTimeout: () {
    throw StateError('dart browser test streams did not finish');
  });
  if (code != 0) {
    throw StateError('dart browser test failed exitCode=$code');
  }
}
