// Copyright (c) 2020, 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 'package:analysis_server/src/services/pub/pub_api.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:http/http.dart';
import 'package:linter/src/rules.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../mocks.dart';
import '../utils/test_code_extensions.dart';
import 'completion.dart';
import 'server_abstract.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(PubspecCompletionTest);
    defineReflectiveTests(AnalysisOptionsCompletionTest);
    defineReflectiveTests(FixDataCompletionTest);
  });
}

@reflectiveTest
class AnalysisOptionsCompletionTest extends AbstractLspAnalysisServerTest
    with CompletionTestMixin {
  @override
  void setUp() {
    registerLintRules();
    super.setUp();
  }

  Future<void> test_nested() async {
    var content = '''
linter:
  rules:
    - ^''';

    var expected = '''
linter:
  rules:
    - annotate_overrides''';

    await verifyCompletions(
      analysisOptionsUri,
      content,
      expectCompletions: ['always_declare_return_types', 'annotate_overrides'],
      applyEditsFor: 'annotate_overrides',
      expectedContent: expected,
    );
  }

  Future<void> test_nested_prefix() async {
    var content = '''
linter:
  rules:
    - ann^''';

    var expected = '''
linter:
  rules:
    - annotate_overrides''';

    await verifyCompletions(
      analysisOptionsUri,
      content,
      expectCompletions: ['annotate_overrides'],
      applyEditsFor: 'annotate_overrides',
      expectedContent: expected,
    );
  }

  Future<void> test_topLevel() async {
    var content = '''
^''';
    var expected = '''
linter: ''';

    await verifyCompletions(
      analysisOptionsUri,
      content,
      expectCompletions: ['linter: '],
      applyEditsFor: 'linter: ',
      expectedContent: expected,
    );
  }

  Future<void> test_topLevel_prefix() async {
    var content = '''
li^''';
    var expected = '''
linter: ''';

    await verifyCompletions(
      analysisOptionsUri,
      content,
      expectCompletions: ['linter: '],
      applyEditsFor: 'linter: ',
      expectedContent: expected,
    );
  }
}

@reflectiveTest
class FixDataCompletionTest extends AbstractLspAnalysisServerTest
    with CompletionTestMixin {
  late Uri fixDataUri;

  @override
  void setUp() {
    super.setUp();
    fixDataUri = toUri(join(projectFolderPath, 'lib', 'fix_data.yaml'));
  }

  Future<void> test_nested() async {
    var content = '''
version: 1.0.0
transforms:
  - changes:
    - ^''';
    var expected = '''
version: 1.0.0
transforms:
  - changes:
    - kind: ''';

    await verifyCompletions(
      fixDataUri,
      content,
      expectCompletions: ['kind: '],
      applyEditsFor: 'kind: ',
      expectedContent: expected,
    );
  }

  Future<void> test_nested_prefix() async {
    var content = '''
version: 1.0.0
transforms:
  - changes:
    - ki^''';
    var expected = '''
version: 1.0.0
transforms:
  - changes:
    - kind: ''';

    await verifyCompletions(
      fixDataUri,
      content,
      expectCompletions: ['kind: '],
      applyEditsFor: 'kind: ',
      expectedContent: expected,
    );
  }

  Future<void> test_topLevel() async {
    var content = '''
version: 1.0.0
^''';
    var expected = '''
version: 1.0.0
transforms:''';

    await verifyCompletions(
      fixDataUri,
      content,
      expectCompletions: ['transforms:'],
      applyEditsFor: 'transforms:',
      expectedContent: expected,
    );
  }

  Future<void> test_topLevel_prefix() async {
    var content = '''
tra^''';
    var expected = '''
transforms:''';

    await verifyCompletions(
      fixDataUri,
      content,
      expectCompletions: ['transforms:'],
      applyEditsFor: 'transforms:',
      expectedContent: expected,
    );
  }
}

@reflectiveTest
class PubspecCompletionTest extends AbstractLspAnalysisServerTest
    with CompletionTestMixin {
  /// Sample package name list JSON in the same format as the API:
  /// https://pub.dev/api/package-name-completion-data
  static const samplePackageList = '''
  { "packages": ["one", "two", "three"] }
  ''';

  /// Sample package details JSON in the same format as the API:
  /// https://pub.dev/api/packages/devtools
  static const samplePackageDetails = '''
  {
    "name":"package",
    "latest":{
      "version":"1.2.3",
      "pubspec":{
        "description":"Description of package"
      }
    }
  }
  ''';

  @override
  void setUp() {
    super.setUp();
    // Cause retries to run immediately.
    PubApi.failedRetryInitialDelaySeconds = 0;
  }

  Future<void> test_insertReplaceRanges() async {
    var content = '''
name: foo
version: 1.0.0

environment:
  s^dk
''';
    var expectedReplaced = '''
name: foo
version: 1.0.0

environment:
  sdk: 
''';
    var expectedInserted = '''
name: foo
version: 1.0.0

environment:
  sdk: dk
''';

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['sdk: '],
      applyEditsFor: 'sdk: ',
      verifyInsertReplaceRanges: true,
      expectedContent: expectedReplaced,
      expectedContentIfInserting: expectedInserted,
    );
  }

  Future<void> test_nested() async {
    var content = '''
name: foo
version: 1.0.0

environment:
  ^''';

    var expected = '''
name: foo
version: 1.0.0

environment:
  sdk: ''';

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['flutter: ', 'sdk: '],
      applyEditsFor: 'sdk: ',
      expectedContent: expected,
    );
  }

  Future<void> test_nested_afterString() async {
    var content = '''
name: foo
  ^''';

    await verifyCompletions(pubspecFileUri, content, expectCompletions: []);
  }

  Future<void> test_nested_prefix() async {
    var content = '''
name: foo
version: 1.0.0

environment:
  sd^''';

    var expected = '''
name: foo
version: 1.0.0

environment:
  sdk: ''';

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['sdk: '],
      applyEditsFor: 'sdk: ',
      expectedContent: expected,
    );
  }

  Future<void> test_package_description() async {
    httpClient.sendHandler = (BaseRequest request) async {
      if (request.url.path.startsWith(PubApi.packageNameListPath)) {
        return Response(samplePackageList, 200);
      } else if (request.url.path.startsWith(PubApi.packageInfoPath)) {
        return Response(samplePackageDetails, 200);
      } else {
        throw UnimplementedError();
      }
    };

    var content = '''
name: foo
version: 1.0.0

dependencies:
  ^''';
    var code = TestCode.parse(content);

    await initialize();
    await openFile(pubspecFileUri, code.code);
    await pumpEventQueue();

    // Descriptions are included in the documentation field that is only added
    // when completions are resolved.
    var completion = await getResolvedCompletion(
      pubspecFileUri,
      code.position.position,
      'one: ',
    );
    expect(
      completion.documentation!.valueEquals('Description of package'),
      isTrue,
    );
  }

  Future<void> test_package_names() async {
    httpClient.sendHandler = (BaseRequest request) async {
      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
        return Response(samplePackageList, 200);
      } else {
        throw UnimplementedError();
      }
    };

    var content = '''
name: foo
version: 1.0.0

dependencies:
  ^''';

    var expected = '''
name: foo
version: 1.0.0

dependencies:
  one: ''';

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['one: ', 'two: ', 'three: '],
      applyEditsFor: 'one: ',
      expectedContent: expected,
    );
  }

  Future<void> test_package_versions_fromApi() async {
    httpClient.sendHandler = (BaseRequest request) async {
      if (request.url.path.startsWith(PubApi.packageNameListPath)) {
        return Response(samplePackageList, 200);
      } else if (request.url.path.startsWith(PubApi.packageInfoPath)) {
        return Response(samplePackageDetails, 200);
      } else {
        throw UnimplementedError();
      }
    };

    var content = '''
name: foo
version: 1.0.0

dependencies:
  ^''';
    var code = TestCode.parse(content);

    var expected = '''
name: foo
version: 1.0.0

dependencies:
  one: ^1.2.3''';

    await initialize();
    await openFile(pubspecFileUri, code.code);

    // Versions are currently only available if we've previously resolved on the
    // package name, so first complete/resolve that.
    var newContent = (await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['one: '],
      resolve: true,
      applyEditsFor: 'one: ',
      openCloseFile: false,
    ))!;
    await replaceFile(222, pubspecFileUri, newContent);

    await verifyCompletions(
      pubspecFileUri,
      newContent.replaceFirst(
        'one: ',
        'one: ^',
      ), // Insert caret at new location
      expectCompletions: ['^1.2.3'],
      applyEditsFor: '^1.2.3',
      expectedContent: expected,
      openCloseFile: false,
    );
  }

  Future<void> test_package_versions_fromPubOutdated() async {
    var json = r'''
    {
      "packages": [
        {
          "package":    "one",
          "latest":     { "version": "3.2.1" },
          "resolvable": { "version": "1.2.4" }
        }
      ]
    }
    ''';
    processRunner.startHandler =
        (executable, args, {workingDirectory, environment}) =>
            MockProcess(1, 0, json, '');

    var content = '''
name: foo
version: 1.0.0

dependencies:
  one: ^''';
    var code = TestCode.parse(content);

    var expected = '''
name: foo
version: 1.0.0

dependencies:
  one: ^1.2.4''';

    await initialize();
    await openFile(pubspecFileUri, code.code);
    await pumpEventQueue(times: 500);

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['^1.2.4', '^3.2.1'],
      applyEditsFor: '^1.2.4',
      expectedContent: expected,
      openCloseFile: false,
    );
  }

  Future<void> test_package_versions_fromPubOutdated_afterChange() async {
    var initialJson = r'''
    {
      "packages": [
        {
          "package":    "one",
          "latest":     { "version": "3.2.1" },
          "resolvable": { "version": "1.2.3" }
        }
      ]
    }
    ''';
    var updatedJson = r'''
    {
      "packages": [
        {
          "package":    "one",
          "latest":     { "version": "2.1.0" },
          "resolvable": { "version": "2.3.4" }
        }
      ]
    }
    ''';
    processRunner.startHandler =
        (executable, args, {workingDirectory, environment}) =>
            MockProcess(1, 0, initialJson, '');

    var content = '''
name: foo
version: 1.0.0

dependencies:
  one: ^''';
    var code = TestCode.parse(content);

    var expected = '''
name: foo
version: 1.0.0

dependencies:
  one: ^2.3.4''';

    var pubspecFile = newFile(pubspecFilePath, content);
    await initialize();
    await openFile(pubspecFileUri, code.code);
    await pumpEventQueue(times: 500);

    // Modify the underlying file which should trigger an update of the
    // cached data.
    processRunner.startHandler =
        (executable, args, {workingDirectory, environment}) =>
            MockProcess(1, 0, updatedJson, '');
    modifyFile2(pubspecFile, '$content# trailing comment');
    await pumpEventQueue(times: 500);

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['^2.1.0', '^2.3.4'],
      applyEditsFor: '^2.3.4',
      expectedContent: expected,
      openCloseFile: false,
    );

    // Also verify the detail fields were populated as expected.
    expect(
      completionResults.singleWhere((c) => c.label == '^2.3.4').detail,
      equals('latest compatible'),
    );
    expect(
      completionResults.singleWhere((c) => c.label == '^2.1.0').detail,
      equals('latest'),
    );
  }

  Future<void> test_package_versions_fromPubOutdated_afterDelete() async {
    var initialJson = r'''
    {
      "packages": [
        {
          "package":    "one",
          "latest":     { "version": "3.2.1" },
          "resolvable": { "version": "1.2.3" }
        }
      ]
    }
    ''';
    processRunner.startHandler =
        (executable, args, {workingDirectory, environment}) =>
            MockProcess(1, 0, initialJson, '');

    var content = '''
name: foo
version: 1.0.0

dependencies:
  one: ^''';
    var code = TestCode.parse(content);

    newFile(pubspecFilePath, content);
    await initialize();
    await openFile(pubspecFileUri, code.code);
    await pumpEventQueue(times: 500);

    // Delete the underlying file which should trigger eviction of the cache.
    deleteFile(pubspecFilePath);
    await pumpEventQueue(times: 500);

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: [],
      openCloseFile: false,
    );

    // There should have been no version numbers.
    expect(completionResults, isEmpty);
  }

  Future<void> test_prefixFilter() async {
    httpClient.sendHandler = (BaseRequest request) async {
      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
        return Response(samplePackageList, 200);
      } else {
        throw UnimplementedError();
      }
    };

    var content = '''
name: foo
version: 1.0.0

dependencies:
  on^''';
    var code = TestCode.parse(content);

    await initialize();
    await openFile(pubspecFileUri, content);
    await pumpEventQueue();

    completionResults = await getCompletion(
      pubspecFileUri,
      code.position.position,
    );
    expect(completionResults.length, equals(1));
    expect(completionResults.single.label, equals('one: '));
  }

  Future<void> test_topLevel() async {
    var content = '''
version: 1.0.0
^''';
    var expected = '''
version: 1.0.0
name: ''';

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['description: ', 'name: '],
      applyEditsFor: 'name: ',
      expectedContent: expected,
    );
  }

  Future<void> test_topLevel_prefix() async {
    var content = '''
na^''';
    var expected = '''
name: ''';

    await verifyCompletions(
      pubspecFileUri,
      content,
      expectCompletions: ['name: '],
      applyEditsFor: 'name: ',
      expectedContent: expected,
    );
  }
}
