// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

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

import 'package:http_multi_server/http_multi_server.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_packages_handler/shelf_packages_handler.dart';
import 'package:shelf_static/shelf_static.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform;
import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
import 'package:web_socket_channel/web_socket_channel.dart';

import '../../../util/math.dart';
import '../../../util/one_off_handler.dart';
import '../../../util/path_handler.dart';
import '../browser_manager.dart';
import 'compiler_support.dart';

/// Support for Dart2Wasm compiled tests.
class Dart2WasmSupport extends CompilerSupport with WasmHtmlWrapper {
  /// Whether [close] has been called.
  bool _closed = false;

  /// The temporary directory in which compiled JS is emitted.
  final _compiledDir = createTempDir();

  /// A map from test suite paths to Futures that will complete once those
  /// suites are finished compiling.
  ///
  /// This is used to make sure that a given test suite is only compiled once
  /// per run, rather than once per browser per run.
  final _compileFutures = <String, Future<void>>{};

  /// The [WasmCompilerPool] managing active instances of `dart2wasm`.
  final _compilerPool = WasmCompilerPool();

  /// The `package:test` side wrapper for the Dart2Wasm runtime.
  final String _jsRuntimeWrapper;

  /// Mappers for Dartifying stack traces, indexed by test path.
  final _mappers = <String, StackTraceMapper>{};

  /// A [PathHandler] used to serve test specific artifacts.
  final _pathHandler = PathHandler();

  /// The root directory served statically by this server.
  final String _root;

  /// Each compiler serves its tests under a different randomly-generated
  /// secret URI to ensure that other users on the same system can't snoop
  /// on data being served through this server, as well as distinguish tests
  /// from different compilers from each other.
  final String _secret = randomUrlSecret();

  /// The underlying server.
  final shelf.Server _server;

  /// A [OneOffHandler] for servicing WebSocket connections for
  /// [BrowserManager]s.
  ///
  /// This is one-off because each [BrowserManager] can only connect to a single
  /// WebSocket.
  final _webSocketHandler = OneOffHandler();

  @override
  Uri get serverUrl => _server.url.resolve('$_secret/');

  Dart2WasmSupport._(
    super.config,
    super.defaultTemplatePath,
    this._jsRuntimeWrapper,
    this._server,
    this._root,
    String faviconPath,
  ) {
    var cascade = shelf.Cascade()
        .add(_webSocketHandler.handler)
        .add(packagesDirHandler())
        .add(_pathHandler.handler)
        .add(createStaticHandler(_root))
        .add(htmlWrapperHandler);

    var pipeline = const shelf.Pipeline()
        .addMiddleware(PathHandler.nestedIn(_secret))
        .addHandler(cascade.handler);

    _server.mount(
      shelf.Cascade().add(createFileHandler(faviconPath)).add(pipeline).handler,
    );
  }

  static Future<Dart2WasmSupport> start({
    required Configuration config,
    required String defaultTemplatePath,
    required String jsRuntimeWrapper,
    required String root,
    required String faviconPath,
  }) async {
    var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
    return Dart2WasmSupport._(
      config,
      defaultTemplatePath,
      jsRuntimeWrapper,
      server,
      root,
      faviconPath,
    );
  }

  @override
  Future<void> compileSuite(
    String dartPath,
    SuiteConfiguration suiteConfig,
    SuitePlatform platform,
  ) {
    return _compileFutures.putIfAbsent(dartPath, () async {
      var dir = Directory(_compiledDir).createTempSync('test_').path;

      var baseCompiledPath = p.join(
        dir,
        '${p.basename(dartPath)}.browser_test.dart',
      );
      var baseUrl =
          '${p.toUri(p.relative(dartPath, from: _root)).path}.browser_test.dart';
      var wasmUrl = '$baseUrl.wasm';
      var sourceMapUrl = '$wasmUrl.map';
      var jsRuntimeWrapperUrl = '$baseUrl.js';
      var jsRuntimeUrl = '$baseUrl.mjs';
      var htmlUrl = '$baseUrl.html';

      var bootstrapContent = '''
        ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
        import 'package:test/src/bootstrap/browser.dart';

        import '${await absoluteUri(dartPath)}' as test;

        void main() {
          internalBootstrapBrowserTest(() => test.main);
        }
      ''';

      await _compilerPool.compile(
        bootstrapContent,
        baseCompiledPath,
        suiteConfig,
      );
      if (_closed) return;

      var wasmPath = '$baseCompiledPath.wasm';
      _pathHandler.add(wasmUrl, (request) {
        return shelf.Response.ok(
          File(wasmPath).readAsBytesSync(),
          headers: {'Content-Type': 'application/wasm'},
        );
      });

      _pathHandler.add(jsRuntimeWrapperUrl, (request) {
        return shelf.Response.ok(
          File(_jsRuntimeWrapper).readAsBytesSync(),
          headers: {'Content-Type': 'application/javascript'},
        );
      });

      var jsRuntimePath = '$baseCompiledPath.mjs';
      _pathHandler.add(jsRuntimeUrl, (request) {
        return shelf.Response.ok(
          File(jsRuntimePath).readAsBytesSync(),
          headers: {'Content-Type': 'application/javascript'},
        );
      });

      var htmlPath = '$baseCompiledPath.html';
      _pathHandler.add(htmlUrl, (request) {
        return shelf.Response.ok(
          File(htmlPath).readAsBytesSync(),
          headers: {'Content-Type': 'text/html'},
        );
      });

      var sourceMapPath = '$wasmPath.map';
      _pathHandler.add(sourceMapUrl, (request) {
        return shelf.Response.ok(
          File(sourceMapPath).readAsBytesSync(),
          headers: {'Content-Type': 'application/json'},
        );
      });
    });
  }

  @override
  Future<void> close() async {
    if (_closed) return;
    _closed = true;
    await Future.wait([
      Directory(_compiledDir).deleteWithRetry(),
      _compilerPool.close(),
      _server.close(),
    ]);
  }

  @override
  StackTraceMapper? stackTraceMapperForPath(String dartPath) =>
      _mappers[dartPath];

  @override
  (Uri, Future<WebSocketChannel>) get webSocket {
    var completer = Completer<WebSocketChannel>.sync();
    // Note: the WebSocketChannel type below is needed for compatibility with
    // package:shelf_web_socket v2.
    var path = _webSocketHandler.create(
      webSocketHandler((WebSocketChannel ws, _) {
        completer.complete(ws);
      }),
    );
    var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path);
    return (webSocketUrl, completer.future);
  }
}
