// Copyright (c) 2017, 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.

/// @docImport 'package:analysis_server/src/analysis_server.dart';
library;

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

import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/src/channel/channel.dart';
import 'package:analysis_server/src/plugin/plugin_isolate.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analyzer/dart/analysis/context_root.dart' as analyzer;
import 'package:analyzer_plugin/protocol/protocol.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin;
import 'package:watcher/watcher.dart';

/// A mock [ServerCommunicationChannel] for testing [AnalysisServer].
class MockServerChannel implements ServerCommunicationChannel {
  /// A controller for the stream of requests and responses from the client to
  /// the server.
  ///
  /// Messages added to this stream should be converted to/from JSON to ensure
  /// they are fully serialized/deserialized as they would be in a real server
  /// otherwise tests may receive real instances where in reality they would be
  /// maps.
  StreamController<RequestOrResponse> requestController =
      StreamController<RequestOrResponse>();

  /// A controller for the stream of requests and responses from the server to
  /// the client.
  ///
  /// Messages added to this stream should be converted to/from JSON to ensure
  /// they are fully serialized/deserialized as they would be in a real server
  /// otherwise tests may receive real instances where in reality they would be
  /// maps.
  StreamController<RequestOrResponse> responseController =
      StreamController<RequestOrResponse>.broadcast();

  /// A controller for the stream of notifications from the server to the
  /// client.
  ///
  /// Unlike [requestController] and [responseController], notifications added
  /// here are not round-tripped through JSON but instead have real class
  /// instances as their `params`. This is because they are only used by tests
  /// which will cast and/or switch on the types for convenience.
  StreamController<Notification> notificationController =
      StreamController<Notification>.broadcast(sync: true);

  Completer<Response>? errorCompleter;

  List<Response> responsesReceived = [];
  List<Notification> notificationsReceived = [];
  List<Request> serverRequestsSent = [];

  bool _closed = false;

  String? name;

  /// True if we are printing out messages exchanged with the server.
  final bool printMessages;

  new({bool? printMessages}) : printMessages = printMessages ?? false;

  /// Return the broadcast stream of notifications.
  Stream<Notification> get notifications {
    return notificationController.stream;
  }

  @override
  Stream<RequestOrResponse> get requests => requestController.stream;

  /// Return the broadcast stream of server-to-client requests.
  Stream<Request> get serverToClientRequests {
    return responseController.stream.where((r) => r is Request).cast<Request>();
  }

  @override
  void close() {
    _closed = true;
  }

  @override
  void sendNotification(Notification notification) {
    // Don't deliver notifications after the connection is closed.
    if (_closed) {
      return;
    }

    notificationsReceived.add(notification);
    notificationController.add(notification);

    var errorCompleter = this.errorCompleter;
    if (errorCompleter != null && notification.event == 'server.error') {
      var params = notification.params!;
      print('[server.error] test: $name message: ${params['message']}');
      errorCompleter.completeError(
        ServerError(params['message'] as String),
        StackTrace.fromString(params['stackTrace'] as String),
      );
    }
  }

  @override
  void sendRequest(Request request) {
    var jsonString = jsonEncode(request.toJson());
    if (printMessages) {
      print('<== $jsonString');
    }

    // Round-trip via JSON to ensure all types are fully serialized as they
    // would be in a real setup.
    request = Request.fromJson(jsonDecode(jsonString) as Map<String, Object?>)!;

    serverRequestsSent.add(request);
    responseController.add(request);
  }

  @override
  void sendResponse(Response response) {
    // Don't deliver responses after the connection is closed.
    if (_closed) {
      return;
    }

    var jsonString = jsonEncode(response.toJson());
    if (printMessages) {
      print('<== $jsonString');
    }

    // Round-trip via JSON to ensure all types are fully serialized as they
    // would be in a real setup.
    response = Response.fromJson(
      jsonDecode(jsonString) as Map<String, Object?>,
    )!;

    responsesReceived.add(response);
    responseController.add(response);
  }

  /// Send the given [request] to the server as if it had been sent from the
  /// client, and return a future that will complete when a response associated
  /// with the [request] has been received.
  ///
  /// The value of the future will be the received response.
  Future<Response> simulateRequestFromClient(Request request) async {
    if (_closed) {
      throw Exception('simulateRequestFromClient after connection closed');
    }

    var jsonString = jsonEncode(request.toJson());
    if (printMessages) {
      print('==> $jsonString');
    }

    // Round-trip via JSON to ensure all types are fully serialized as they
    // would be in a real setup.
    request = Request.fromJson(jsonDecode(jsonString) as Map<String, Object?>)!;

    requestController.add(request);
    var response = await waitForResponse(request);

    // Round-trip via JSON to ensure all types are fully serialized as they
    // would be in a real setup.
    response = Response.fromJson(
      jsonDecode(jsonEncode(response)) as Map<String, Object?>,
    )!;

    return response;
  }

  /// Send the given [response] to the server as if it had been sent from the
  /// client.
  void simulateResponseFromClient(Response response) {
    // No further requests should be sent after the connection is closed.
    if (_closed) {
      throw Exception('simulateRequestFromClient after connection closed');
    }

    var jsonString = jsonEncode(response.toJson());
    if (printMessages) {
      print('==> $jsonString');
    }

    // Round-trip via JSON to ensure all types are fully serialized as they
    // would be in a real setup.
    response = Response.fromJson(
      jsonDecode(jsonString) as Map<String, Object?>,
    )!;

    requestController.add(response);
  }

  /// Return a future that will complete when a response associated with the
  /// given [request] has been received. The value of the future will be the
  /// received response.
  ///
  /// Unlike [simulateRequestFromClient], this method assumes that the [request]
  /// has already been sent to the server.
  Future<Response> waitForResponse(Request request) {
    var id = request.id;
    return responseController.stream
        .where((r) => r is Response)
        .cast<Response>()
        .firstWhere((response) => response.id == id);
  }
}

class ServerError implements Exception {
  final String message;

  new(this.message);

  @override
  String toString() {
    return 'Server Error: $message';
  }
}

/// A plugin manager that simulates broadcasting requests to plugins by
/// hard-coding the responses.
class TestPluginManager implements PluginManager {
  plugin.AnalysisSetAnalysisRootsParams? analysisSetAnalysisRootsParams;
  plugin.AnalysisSetPriorityFilesParams? analysisSetPriorityFilesParams;
  plugin.AnalysisSetSubscriptionsParams? analysisSetSubscriptionsParams;
  plugin.AnalysisUpdateContentParams? analysisUpdateContentParams;
  plugin.RequestParams? broadcastedRequest;
  Map<PluginIsolate, Future<plugin.Response>>? broadcastResults;
  Map<PluginIsolate, Future<plugin.Response>>? Function(plugin.RequestParams)?
  handleRequest;
  Map<analyzer.ContextRoot, List<String>> contextRootPlugins = {};

  @override
  List<PluginIsolate> pluginIsolates = [];

  @override
  Completer<void> initializedCompleter = Completer();

  StreamController<void> pluginsChangedController =
      StreamController.broadcast();

  @override
  final contextRootsWithNoPlugins = <String>{};

  @override
  List<PluginIsolate> get legacyPluginIsolates =>
      pluginIsolates.where((e) => e.isLegacy).toList();

  @override
  List<PluginIsolate> get newPluginIsolates =>
      pluginIsolates.where((e) => !e.isLegacy).toList();

  @override
  Stream<void> get pluginsChanged => pluginsChangedController.stream;

  @override
  Future<void> addPluginToContextRoot(
    analyzer.ContextRoot contextRoot,
    String path, {
    required bool isLegacyPlugin,
  }) async {
    contextRootPlugins.putIfAbsent(contextRoot, () => []).add(path);
  }

  @override
  Map<PluginIsolate, Future<plugin.Response>> broadcastRequest(
    plugin.RequestParams params, {
    analyzer.ContextRoot? contextRoot,
  }) {
    broadcastedRequest = params;
    return handleRequest?.call(params) ?? broadcastResults ?? {};
  }

  @override
  List<Future<plugin.Response>> broadcastWatchEvent(WatchEvent watchEvent) {
    return [];
  }

  @override
  dynamic noSuchMethod(Invocation invocation) =>
      throw Exception('Unexpected invocation of ${invocation.memberName}');

  @override
  String pluginStateFolderPath(String pluginPath) => '/some/path';

  @override
  void removedContextRoot(analyzer.ContextRoot contextRoot) {
    contextRootPlugins.remove(contextRoot);
  }

  @override
  Future<void> restartPlugins() async {
    // Nothing to restart.
  }

  @override
  void setAnalysisSetAnalysisRootsParams(
    plugin.AnalysisSetAnalysisRootsParams params,
  ) {
    analysisSetAnalysisRootsParams = params;
  }

  @override
  void setAnalysisSetPriorityFilesParams(
    plugin.AnalysisSetPriorityFilesParams params,
  ) {
    analysisSetPriorityFilesParams = params;
  }

  @override
  void setAnalysisSetSubscriptionsParams(
    plugin.AnalysisSetSubscriptionsParams params,
  ) {
    analysisSetSubscriptionsParams = params;
  }

  @override
  void setAnalysisUpdateContentParams(
    plugin.AnalysisUpdateContentParams params, {
    String? precomputedNewContentForChange,
  }) {
    analysisUpdateContentParams = params;
  }
}
