// 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 Compiler, StackTraceMapper, SuitePlatform;
import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/stack_trace_mapper.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/package_map.dart';
import '../../../util/path_handler.dart';
import 'compiler_support.dart';

class JsPrecompiledSupport = PrecompiledSupport with JsHtmlWrapper;
class WasmPrecompiledSupport = PrecompiledSupport with WasmHtmlWrapper;

/// Support for precompiled test files.
abstract class PrecompiledSupport extends CompilerSupport {
  /// Whether [close] has been called.
  bool _closed = false;

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

  /// The root directory served statically by the 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();

  /// The URL at which this compiler serves its tests.
  ///
  /// Each compiler serves its tests under a different directory.
  @override
  Uri get serverUrl => _server.url.resolve('$_secret/');

  PrecompiledSupport._(super.config, super.defaultTemplatePath, this._server,
      this._root, String faviconPath) {
    var cascade = shelf.Cascade()
        .add(_webSocketHandler.handler)
        .add(createStaticHandler(_root, serveFilesOutsidePath: true))
        // TODO: This packages dir handler should not be necessary?
        .add(packagesDirHandler())
        // Even for precompiled tests, we will auto-create a bootstrap html file
        // if none was present.
        .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<PrecompiledSupport> start({
    required Compiler compiler,
    required Configuration config,
    required String defaultTemplatePath,
    required String root,
    required String faviconPath,
  }) async {
    var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));

    return switch (compiler) {
      Compiler.dart2js => JsPrecompiledSupport._(
          config, defaultTemplatePath, server, root, faviconPath),
      Compiler.dart2wasm => WasmPrecompiledSupport._(
          config, defaultTemplatePath, server, root, faviconPath),
      Compiler.exe ||
      Compiler.kernel ||
      Compiler.source =>
        throw UnsupportedError(
            'The browser platform does not support $compiler'),
    };
  }

  /// Compiles [dartPath] using [suiteConfig] for [platform].
  @override
  Future<void> compileSuite(String dartPath, SuiteConfiguration suiteConfig,
      SuitePlatform platform) async {
    if (suiteConfig.jsTrace) return;
    var mapPath = p.join(
        suiteConfig.precompiledPath!, '$dartPath.browser_test.dart.js.map');
    var mapFile = File(mapPath);
    if (mapFile.existsSync()) {
      _mappers[dartPath] = JSStackTraceMapper(mapFile.readAsStringSync(),
          mapUrl: p.toUri(mapPath),
          sdkRoot: Uri.parse(r'/packages/$sdk'),
          packageMap: (await currentPackageConfig).toPackageMap());
    }
  }

  /// Retrieves a stack trace mapper for [path] if available.
  @override
  StackTraceMapper? stackTraceMapperForPath(String dartPath) =>
      _mappers[dartPath];

  /// Closes down anything necessary for this implementation.
  @override
  Future<void> close() async {
    if (_closed) return;
    _closed = true;
    await _server.close();
  }

  @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);
  }
}
