// Copyright (c) 2014, 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:convert';
import 'dart:io';

import 'package:analysis_server/src/plugin/notification_manager.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/utilities/process.dart';
import 'package:analyzer_plugin/protocol/protocol.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
import 'package:http/http.dart' as http;
import 'package:test/test.dart';

/// A [Matcher] that check that the given [Response] has an expected identifier
/// and has an error.  The error code may optionally be checked.
Matcher isResponseFailure(String id, [RequestErrorCode? code]) =>
    _IsResponseFailure(id, code);

/// A [Matcher] that check that the given [Response] has an expected identifier
/// and no error.
Matcher isResponseSuccess(String id) => _IsResponseSuccess(id);

class MockHttpClient extends http.BaseClient {
  late Future<http.Response> Function(http.BaseRequest request) sendHandler;
  int sendHandlerCalls = 0;
  bool wasClosed = false;

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

  @override
  dynamic noSuchMethod(Invocation invocation) {
    return super.noSuchMethod(invocation);
  }

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) {
    if (wasClosed) {
      throw Exception('get() called after close()');
    }

    return sendHandler(request)
        .then(
          (resp) => http.StreamedResponse(
            Stream.value(resp.body.codeUnits),
            resp.statusCode,
          ),
        )
        .whenComplete(() => sendHandlerCalls++);
  }
}

class MockProcess implements Process {
  static int killedExitCode = -1;

  final int _pid;
  final _exitCodeCompleter = Completer<int>();
  final String _stdout, _stderr;

  new(this._pid, FutureOr<int> exitCode, this._stdout, this._stderr) {
    Future.value(exitCode).then(_exitCodeCompleter.complete);
  }

  @override
  Future<int> get exitCode => _exitCodeCompleter.future;

  @override
  int get pid => _pid;

  @override
  Stream<List<int>> get stderr => Stream<List<int>>.value(utf8.encode(_stderr));

  @override
  Stream<List<int>> get stdout => Stream<List<int>>.value(utf8.encode(_stdout));

  @override
  bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
    _exitCodeCompleter.complete(killedExitCode);
    return true;
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    return super.noSuchMethod(invocation);
  }
}

class MockProcessRunner implements ProcessRunner {
  @override
  final Map<String, String>? environment = null;

  ProcessResult Function(
    String executable,
    List<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
    Encoding? stderrEncoding,
    Encoding? stdoutEncoding,
  })
  runSyncHandler =
      (_, _, {workingDirectory, environment, stderrEncoding, stdoutEncoding}) =>
          throw UnimplementedError();

  FutureOr<Process> Function(
    String executable,
    List<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
  })
  startHandler = (_, _, {workingDirectory, environment}) =>
      throw UnimplementedError();

  @override
  ProcessResult runSync(
    String executable,
    List<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
    Encoding? stderrEncoding,
    Encoding? stdoutEncoding,
  }) => runSyncHandler(
    executable,
    arguments,
    workingDirectory: workingDirectory,
    environment: environment,
    stderrEncoding: stderrEncoding,
    stdoutEncoding: stdoutEncoding,
  );

  @override
  Future<Process> start(
    String executable,
    List<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
    bool includeParentEnvironment = true,
    bool runInShell = false,
    ProcessStartMode mode = ProcessStartMode.normal,
  }) async {
    return await startHandler(
      executable,
      arguments,
      workingDirectory: workingDirectory,
      environment: environment,
    );
  }
}

class TestNotificationManager implements AbstractNotificationManager {
  List<plugin.Notification> notifications = [];

  Map<String, Map<String, List<protocol.AnalysisError>>> recordedErrors = {};

  List<String> pluginErrors = [];

  @override
  Stream<PluginPrint> pluginPrints = Stream.empty();

  @override
  void handlePluginError(String message) {
    pluginErrors.add(message);
  }

  @override
  void handlePluginNotification(
    String pluginId,
    plugin.Notification notification,
  ) {
    notifications.add(notification);
  }

  @override
  dynamic noSuchMethod(Invocation invocation) {
    fail('Unexpected invocation of ${invocation.memberName}');
  }

  @override
  void recordAnalysisErrors(
    String pluginId,
    String filePath,
    List<protocol.AnalysisError> errorData,
  ) {
    recordedErrors.putIfAbsent(pluginId, () => {})[filePath] = errorData;
  }
}

/// A [Matcher] that check that there are no `error` in a given [Response].
class _IsResponseFailure extends Matcher {
  final String _id;
  final RequestErrorCode? _code;

  new(this._id, this._code);

  @override
  Description describe(Description description) {
    description = description.add(
      'response with identifier "$_id" and an error',
    );
    var code = _code;
    if (code != null) {
      description = description.add(' with code ${code.name}');
    }
    return description;
  }

  @override
  Description describeMismatch(
    Object? item,
    Description mismatchDescription,
    Map<Object?, Object?> matchState,
    bool verbose,
  ) {
    var response = item as Response;
    var id = response.id;
    var error = response.error;
    mismatchDescription.add('has identifier "$id"');
    if (error == null) {
      mismatchDescription.add(' and has no error');
    } else {
      mismatchDescription.add(' and has error code ${error.code.name}');
    }
    return mismatchDescription;
  }

  @override
  bool matches(Object? item, Map<Object?, Object?> matchState) {
    var response = item as Response;
    var error = response.error;
    if (response.id != _id || error == null) {
      return false;
    }
    if (_code != null && error.code != _code) {
      return false;
    }
    return true;
  }
}

/// A [Matcher] that check that there are no `error` in a given [Response].
class _IsResponseSuccess extends Matcher {
  final String _id;

  new(this._id);

  @override
  Description describe(Description description) {
    return description.addDescriptionOf(
      'response with identifier "$_id" and without error',
    );
  }

  @override
  Description describeMismatch(
    Object? item,
    Description mismatchDescription,
    Map<Object?, Object?> matchState,
    bool verbose,
  ) {
    var response = item as Response?;
    if (response == null) {
      mismatchDescription.add('is null response');
    } else {
      var id = response.id;
      var error = response.error;
      mismatchDescription.add('has identifier "$id"');
      if (error != null) {
        mismatchDescription.add(' and has error $error');
      }
    }
    return mismatchDescription;
  }

  @override
  bool matches(Object? item, Map<Object?, Object?> matchState) {
    var response = item as Response?;
    return response != null && response.id == _id && response.error == null;
  }
}
