// 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 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/protocol/protocol_generated.dart'
    hide MessageType;
import 'package:analysis_server/src/analysis_server.dart' show MessageType;
import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manager.dart';
import 'package:analyzer/src/utilities/cancellation.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'analysis_server_base.dart';
import 'constants.dart';
import 'mocks.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(ServerDomainTest);
    defineReflectiveTests(ServerDartFixPromptTest);
  });
}

/// Checks server interacts with [DartFixPromptManager] correctly.
///
/// Tests for [DartFixPromptManager]'s behaviour are in
/// test/services/user_prompts/dart_fix_prompt_manager_test.dart.
@reflectiveTest
class ServerDartFixPromptTest extends PubPackageAnalysisServerTest {
  late TestDartFixPromptManager promptManager;

  @override
  DartFixPromptManager? get dartFixPromptManager => promptManager;

  @override
  void setUp() {
    promptManager = TestDartFixPromptManager();
    super.setUp();
  }

  Future<void> test_trigger_afterInitialAnalysis() async {
    await setRoots(included: [workspaceRootPath], excluded: []);
    await pumpEventQueue(times: 5000);
    expect(promptManager.checksTriggered, 1);
  }

  Future<void> test_trigger_afterPackageConfigChange() async {
    // Ensure there's a file to analyze otherwise writing the package_config
    // won't trigger any additional analysis.
    newFile('$testPackageLibPath/test.dart', 'void f() {}');

    // Set up and let initial analysis complete.
    await setRoots(included: [workspaceRootPath], excluded: []);
    await pumpEventQueue(times: 5000);
    expect(promptManager.checksTriggered, 1);

    // Expect that writing package config attempts to trigger another check.
    writeTestPackageConfig();
    await pumpEventQueue(times: 5000);
    expect(promptManager.checksTriggered, 2);
  }
}

@reflectiveTest
class ServerDomainTest extends PubPackageAnalysisServerTest {
  Future<void> test_getVersion() async {
    var request = ServerGetVersionParams().toRequest(
      '0',
      clientUriConverter: server.uriConverter,
    );
    var response = await handleSuccessfulRequest(request);
    expect(
      response.toJson(),
      equals({
        Response.idAttributeName: '0',
        Response.resultAttributeName: {versionKey: PROTOCOL_VERSION},
      }),
    );
  }

  Future<void> test_openUrl() async {
    server.clientCapabilities.requests = ['openUrlRequest'];

    // Send the request.
    var uri = toUri('https://dart.dev');
    var responseFuture = server.openUriNotificationSender!.call(uri);
    expect(serverChannel.serverRequestsSent, hasLength(1));

    // Simulate the response.
    var request = serverChannel.serverRequestsSent[0];
    serverChannel.simulateResponseFromClient(
      ServerOpenUrlRequestResult().toResponse(
        request.id,
        clientUriConverter: server.uriConverter,
      ),
    );
    await responseFuture;
  }

  Future<void> test_setClientCapabilities_lspCapabilities() async {
    // Test an arbitrary set of capabilities.
    var capabilities = lsp.ClientCapabilities(
      textDocument: lsp.TextDocumentClientCapabilities(
        hover: lsp.HoverClientCapabilities(
          contentFormat: [lsp.MarkupKind.PlainText],
        ),
      ),
      workspace: lsp.WorkspaceClientCapabilities(
        applyEdit: true,
        workspaceEdit: lsp.WorkspaceEditClientCapabilities(
          documentChanges: true,
          resourceOperations: [lsp.ResourceOperationKind.Create],
        ),
      ),
    );

    var request = ServerSetClientCapabilitiesParams(
      [],
      lspCapabilities: capabilities.toJson(),
    ).toRequest('1', clientUriConverter: server.uriConverter);

    await handleSuccessfulRequest(request);
    var effectiveCapabilities = server.editorClientCapabilities;
    expect(
      effectiveCapabilities.hoverContentFormats,
      equals([lsp.MarkupKind.PlainText]),
    );
    expect(effectiveCapabilities.applyEdit, isTrue);
    expect(effectiveCapabilities.documentChanges, isTrue);
    expect(effectiveCapabilities.createResourceOperations, isTrue);
  }

  Future<void> test_setClientCapabilities_lspCapabilities_invalid() async {
    var request = ServerSetClientCapabilitiesParams(
      [],
      lspCapabilities: {
        'textDocument': 1, // Not valid
      },
    ).toRequest('1', clientUriConverter: server.uriConverter);

    var response = await handleRequest(request);
    expect(
      response,
      isResponseFailure('1', RequestErrorCode.INVALID_PARAMETER),
    );
    expect(
      response.error!.message,
      "The 'lspCapabilities' parameter was invalid:"
      ' textDocument must be of type TextDocumentClientCapabilities',
    );
  }

  Future<void> test_setClientCapabilities_requests() async {
    var requestId = -1;

    Future<void> setCapabilities({
      required bool openUrlRequest,
      required bool showMessageRequest,
    }) async {
      var requests = [
        if (openUrlRequest) 'openUrlRequest',
        if (showMessageRequest) 'showMessageRequest',
      ];
      if (requestId >= 0) {
        // This is a bit of a kludge, but the first time this function is called
        // we won't set the request, we'll just test the default state.
        var request = ServerSetClientCapabilitiesParams(requests).toRequest(
          requestId.toString(),
          clientUriConverter: server.uriConverter,
        );
        await handleSuccessfulRequest(request);
      }
      requestId++;

      expect(server.clientCapabilities.requests, requests);
      expect(
        server.openUriNotificationSender,
        openUrlRequest ? isNotNull : isNull,
      );
      expect(server.userPromptSender, showMessageRequest ? isNotNull : isNull);
    }

    await setCapabilities(openUrlRequest: false, showMessageRequest: false);
    await setCapabilities(openUrlRequest: true, showMessageRequest: false);
    await setCapabilities(openUrlRequest: true, showMessageRequest: true);
    await setCapabilities(openUrlRequest: false, showMessageRequest: true);
    await setCapabilities(openUrlRequest: false, showMessageRequest: false);
  }

  /// Verify that the server handles URIs once we've enabled the supportsUris
  /// client capability.
  Future<void>
  test_setClientCapabilities_supportsUris_clientToServer_request() async {
    // Tell the server we support URIs.
    await handleSuccessfulRequest(
      ServerSetClientCapabilitiesParams(
        [],
        supportsUris: true,
      ).toRequest('-1', clientUriConverter: server.uriConverter),
    );

    // Set the roots using a URI. Since the helper methods will to through
    // toJson() (which will convert paths to URIs) we need to pass the JSON
    // manually here.
    await handleSuccessfulRequest(
      Request('1', 'analysis.setAnalysisRoots', {
        'included': [toUri(workspaceRootPath).toString()],
        'excluded': [],
      }),
    );
    await pumpEventQueue(times: 5000);

    // Ensure the roots were recorded correctly.
    expect(server.contextManager.includedPaths, [
      convertPath(workspaceRootPath),
    ]);
  }

  Future<void> test_setClientCapabilities_supportsUris_defaults() async {
    // Before request.
    expect(server.clientCapabilities.supportsUris, isNull);
    expect(server.uriConverter.supportedNonFileSchemes, isEmpty);

    // If not supplied.
    await handleSuccessfulRequest(
      ServerSetClientCapabilitiesParams(
        [],
      ).toRequest('-1', clientUriConverter: server.uriConverter),
    );
    expect(server.clientCapabilities.supportsUris, isNull);
    expect(server.uriConverter.supportedNonFileSchemes, isEmpty);

    // If set explicitly to false.
    await handleSuccessfulRequest(
      ServerSetClientCapabilitiesParams(
        [],
        supportsUris: false,
      ).toRequest('-1', clientUriConverter: server.uriConverter),
    );
    expect(server.clientCapabilities.supportsUris, isFalse);
    expect(server.uriConverter.supportedNonFileSchemes, isEmpty);
  }

  Future<void>
  test_setClientCapabilities_supportsUris_false_rejectsUris() async {
    // Explicitly tell the server we do not support URIs.
    await handleSuccessfulRequest(
      ServerSetClientCapabilitiesParams(
        [],
        supportsUris: false,
      ).toRequest('-1', clientUriConverter: server.uriConverter),
    );

    // Try to send a URI anyway.
    var request = Request('1', 'analysis.setAnalysisRoots', {
      'included': [toUri(workspaceRootPath).toString()],
      'excluded': [],
    });
    var response = await handleRequest(request);
    expect(
      response,
      isResponseFailure(request.id, RequestErrorCode.INVALID_FILE_PATH_FORMAT),
    );
  }

  /// Verify that the server uses URIs in notifications once we've enabled the
  /// supportsUris client capability.
  Future<void>
  test_setClientCapabilities_supportsUris_serverToClient_notification() async {
    // Add a file with an error for testing.
    var testFilePath = convertPath('$testPackageLibPath/test.dart');
    var testFileUriString = toUri(testFilePath).toString();
    newFile(testFilePath, 'broken');

    // Tell the server we support URIs before analysis starts since we will
    // verify the analysis.errors notification.
    await handleSuccessfulRequest(
      ServerSetClientCapabilitiesParams(
        [],
        supportsUris: true,
      ).toRequest('10', clientUriConverter: server.uriConverter),
    );
    await pumpEventQueue(times: 5000);

    // Trigger analysis.
    await setRoots(
      // We can use paths here because toJson() will handle the conversion.
      included: [workspaceRootPath],
      excluded: [],
    );
    await pumpEventQueue(times: 5000);

    // Verify the last error for this file was using a URI.
    var lastErrorFile = serverChannel.notificationsReceived
        .where((notification) => notification.event == 'analysis.errors')
        .map((notification) => notification.params!['file'] as String)
        .lastWhere((filePath) => filePath.endsWith('test.dart'));
    expect(lastErrorFile, testFileUriString);
  }

  /// Verify that the server returns URIs once we've enabled the supportsUris
  /// client capability.
  Future<void>
  test_setClientCapabilities_supportsUris_serverToClient_response() async {
    // Add a file with an error for testing.
    var testFilePath = convertPath('$testPackageLibPath/test.dart');
    var testFileUriString = toUri(testFilePath).toString();
    newFile(testFilePath, 'broken');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await pumpEventQueue(times: 5000);

    // Tell the server we support URIs.
    await handleSuccessfulRequest(
      ServerSetClientCapabilitiesParams(
        [],
        supportsUris: true,
      ).toRequest('10', clientUriConverter: server.uriConverter),
    );
    await pumpEventQueue(times: 5000);

    // Send a GetErrors request. The response has nested FilePaths inside the
    // AnalysisErrors so will confirm the server mapped correctly.
    var response = await handleSuccessfulRequest(
      Request('1', 'analysis.getErrors', {'file': testFileUriString}),
    );
    // Verify the error location was the expected URI.
    expect(
      (response.result as dynamic)['errors'][0]!['location']['file'],
      testFileUriString,
    );
  }

  Future<void>
  test_setClientCapabilities_supportsUris_unspecified_rejectsUris() async {
    // Do not tell the server we support URIs.

    // Try to send a URI anyway.
    var request = Request('1', 'analysis.setAnalysisRoots', {
      'included': [toUri(workspaceRootPath).toString()],
      'excluded': [],
    });
    var response = await handleRequest(request);
    expect(
      response,
      isResponseFailure(request.id, RequestErrorCode.INVALID_FILE_PATH_FORMAT),
    );
  }

  Future<void> test_setSubscriptions_invalidServiceName() async {
    var request = Request('0', serverRequestSetSubscriptions, {
      subscriptionsKey: ['noSuchService'],
    });
    var response = await handleRequest(request);
    expect(response, isResponseFailure('0'));
  }

  Future<void> test_setSubscriptions_success() async {
    expect(server.serverServices, isEmpty);
    // send request
    var request = ServerSetSubscriptionsParams([
      ServerService.STATUS,
    ]).toRequest('0', clientUriConverter: server.uriConverter);
    await handleSuccessfulRequest(request);
    // set of services has been changed
    expect(server.serverServices, contains(ServerService.STATUS));
  }

  Future<void> test_showMessage() async {
    server.clientCapabilities.requests = ['showMessageRequest'];

    // Send the request.
    var responseFuture = server.userPromptSender!(
      MessageType.warning,
      'message',
      ['a', 'b'],
      NotCancelableToken(),
    );
    expect(serverChannel.serverRequestsSent, hasLength(1));

    // Simulate the response.
    var request = serverChannel.serverRequestsSent[0];
    serverChannel.simulateResponseFromClient(
      ServerShowMessageRequestResult(
        action: 'a',
      ).toResponse(request.id, clientUriConverter: server.uriConverter),
    );
    var response = await responseFuture;
    expect(response, 'a');
  }

  Future<void> test_showMessage_nullResponse() async {
    server.clientCapabilities.requests = ['showMessageRequest'];

    // Send the request.
    var responseFuture = server.userPromptSender!(
      MessageType.warning,
      'message',
      ['a', 'b'],
      NotCancelableToken(),
    );
    expect(serverChannel.serverRequestsSent, hasLength(1));

    // Simulate the response.
    var request = serverChannel.serverRequestsSent[0];
    serverChannel.simulateResponseFromClient(
      ServerShowMessageRequestResult().toResponse(
        request.id,
        clientUriConverter: server.uriConverter,
      ),
    );
    var response = await responseFuture;
    expect(response, isNull);
  }

  Future<void> test_shutdown() async {
    var request = ServerShutdownParams().toRequest(
      '0',
      clientUriConverter: server.uriConverter,
    );
    await handleSuccessfulRequest(request);
  }
}

class TestDartFixPromptManager implements DartFixPromptManager {
  var checksTriggered = 0;

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);

  @override
  void triggerCheck() {
    checksTriggered++;
  }
}
