// 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 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_testing/package_config_file_builder.dart';
import 'package:linter/src/rules.dart';
import 'package:path/path.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../analysis_server_base.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(NotificationErrorsTest);
  });
}

@reflectiveTest
class NotificationErrorsTest extends PubPackageAnalysisServerTest {
  Map<File, List<AnalysisError>?> filesErrors = {};

  @override
  void processNotification(Notification notification) {
    if (notification.event == analysisNotificationErrors) {
      var decoded = AnalysisErrorsParams.fromNotification(
        notification,
        clientUriConverter: server.uriConverter,
      );
      filesErrors[getFile(decoded.file)] = decoded.errors;
    } else if (notification.event == analysisNotificationFlushResults) {
      var decoded = AnalysisFlushResultsParams.fromNotification(
        notification,
        clientUriConverter: server.uriConverter,
      );
      for (var file in decoded.files) {
        filesErrors[getFile(file)] = null;
      }
    }
  }

  @override
  void setUp() {
    registerLintRules();
    super.setUp();
  }

  Future<void> test_analysisOptionsFile() async {
    var analysisOptions = newAnalysisOptionsYamlFile(testPackageRootPath, '''
linter:
  rules:
    - invalid_lint_rule_name
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();
    await pumpEventQueue();
    //
    // Verify the error result.
    //
    var errors = filesErrors[analysisOptions]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, analysisOptions.path);
    expect(error.severity, AnalysisErrorSeverity.WARNING);
    expect(error.type, AnalysisErrorType.STATIC_WARNING);
  }

  Future<void> test_analysisOptionsFile_packageInclude() async {
    var analysisOptions = newAnalysisOptionsYamlFile(testPackageRootPath, '''
include: package:lints/lints.yaml
''');

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

    // Verify there's an error for the import.
    var errors = filesErrors[analysisOptions]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, analysisOptions.path);
    expect(error.severity, AnalysisErrorSeverity.WARNING);
    expect(error.type, AnalysisErrorType.STATIC_WARNING);

    // Write a package file that allows resolving the include.
    var lintsRootPath = '/packages/lints';
    newFile(
      '$lintsRootPath/lib/lints.yaml',
      '# Used for testing diagnostics for imports in analysis options files.',
    );
    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'lints', rootFolder: getFolder(lintsRootPath)),
    );

    // Ensure the errors disappear.
    await waitForTasksFinished();
    await pumpEventQueue();
    errors = filesErrors[analysisOptions]!;
    expect(errors, hasLength(0));
  }

  Future<void> test_androidManifestFile() async {
    var manifestPath = join(
      testPackageRootPath,
      'android',
      'AndroidManifest.xml',
    );
    var manifestFile = newFile(manifestPath, '''
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
    <uses-feature android:name="android.software.home_screen" />
</manifest>
''');
    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  optional-checks:
    chrome-os-manifest-checks: true
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();
    await pumpEventQueue();
    //
    // Verify the error result.
    //
    var errors = filesErrors[manifestFile]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, manifestFile.path);
    expect(error.severity, AnalysisErrorSeverity.WARNING);
    expect(error.type, AnalysisErrorType.STATIC_WARNING);
  }

  Future<void> test_androidManifestFile_dotDirectoryIgnored() async {
    var manifestPath = join(
      testPackageRootPath,
      'ios',
      '.symlinks',
      'AndroidManifest.xml',
    );
    var manifestFile = newFile(manifestPath, '''
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
    <uses-feature android:name="android.software.home_screen" />
</manifest>
''');
    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  optional-checks:
    chrome-os-manifest-checks: true
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();
    await pumpEventQueue();
    //
    // Verify that the file wasn't analyzed.
    //
    var errors = filesErrors[manifestFile];
    expect(errors, isNull);
  }

  Future<void> test_dartToolGeneratedProject_referencedByUserProject() async {
    // Although errors are not generated for dotfolders, their contents should
    // still be analyzed so that code that references them (for example
    // flutter_gen) should still be updated.
    var configPath = join(
      testPackageRootPath,
      '.dart_tool/package_config.json',
    );
    var generatedProject = join(testPackageRootPath, '.dart_tool/foo');
    var generatedFilePath = join(generatedProject, 'lib', 'foo.dart');

    // Add the generated project into package_config.json.
    var config = PackageConfigFileBuilder();
    config.add(name: 'foo', rootFolder: getFolder(generatedProject));
    newFile(configPath, config.toContent());

    // Set up project that references the class prior to initial analysis.
    var generatedFile = newFile(generatedFilePath, 'class A {}');
    addTestFile('''
import 'package:foo/foo.dart';
A? a;
    ''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[testFile], isEmpty);

    // Remove the class, which should cause the main project to have an analysis
    // error.
    modifyFile2(generatedFile, '');

    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[testFile], isNotEmpty);
  }

  Future<void> test_dataFile() async {
    var dataFile = newFile('$testPackageLibPath/fix_data.yaml', '''
version: 1
transforms:
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();
    await pumpEventQueue();
    //
    // Verify the error result.
    //
    var errors = filesErrors[dataFile]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, dataFile.path);
    expect(error.severity, AnalysisErrorSeverity.ERROR);
    expect(error.type, AnalysisErrorType.COMPILE_TIME_ERROR);
  }

  Future<void> test_dotFolder_priority() async {
    // Files inside dotFolders should not generate error notifications even
    // if they are added to priority (priority affects only priority, not what
    // is analyzed).
    await setRoots(included: [workspaceRootPath], excluded: []);
    addTestFile('');
    var brokenFile = newFile(
      join(testPackageRootPath, '.dart_tool/broken.dart'),
      'err',
    );

    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[brokenFile], isNull);

    // Add to priority files and give chance for the file to be analyzed (if
    // it would).
    setPriorityFiles([brokenFile]);
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // There should still be no errors.
    expect(filesErrors[brokenFile], isNull);
  }

  Future<void> test_dotFolder_unopenedFile() async {
    // Files inside dotFolders are not analyzed. Sending requests that cause
    // them to be opened (such as hovers) should not result in error notifications
    // because there is no event that would flush them and they'd remain in the
    // editor forever.
    await setRoots(included: [workspaceRootPath], excluded: []);
    addTestFile('');
    var brokenFile = newFile(
      '$testPackageRootPath/.dart_tool/broken.dart',
      'err',
    );

    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[brokenFile], isNull);

    // Send a getHover request for the file that will cause it to be read from disk.
    await handleSuccessfulRequest(
      AnalysisGetHoverParams(
        brokenFile.path,
        0,
      ).toRequest('0', clientUriConverter: server.uriConverter),
    );
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // There should be no errors because the file is not being analyzed.
    expect(filesErrors[brokenFile], isNull);
  }

  Future<void> test_excludedFolder() async {
    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  exclude:
    - excluded/**
''');
    await setRoots(included: [workspaceRootPath], excluded: []);
    var excludedFile = newFile(
      '$testPackageRootPath/excluded/broken.dart',
      'err',
    );

    // There should be no errors initially.
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[excludedFile], isNull);

    // Triggering the file to be processed should still generate no errors.
    await handleSuccessfulRequest(
      AnalysisGetHoverParams(
        excludedFile.path,
        0,
      ).toRequest('0', clientUriConverter: server.uriConverter),
    );
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[excludedFile], isNull);

    // Opening the file should still generate no errors.
    await handleSuccessfulRequest(
      AnalysisSetPriorityFilesParams([
        excludedFile.path,
      ]).toRequest('0', clientUriConverter: server.uriConverter),
    );
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[excludedFile], isNull);
  }

  Future<void> test_importError() async {
    await setRoots(included: [workspaceRootPath], excluded: []);

    addTestFile('''
import 'does_not_exist.dart';
''');
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    var errors = filesErrors[testFile]!;
    // Verify that we are generating only 1 error for the bad URI.
    // https://github.com/dart-lang/sdk/issues/23754
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.severity, AnalysisErrorSeverity.ERROR);
    expect(error.type, AnalysisErrorType.COMPILE_TIME_ERROR);
    expect(error.message, startsWith("Target of URI doesn't exist"));
  }

  Future<void> test_lintError() async {
    var camelCaseTypesLintName = 'camel_case_types';

    newAnalysisOptionsYamlFile(testPackageRootPath, '''
linter:
  rules:
    - $camelCaseTypesLintName
''');

    addTestFile('class a { }');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    var lints = testFileAnalysisOptions.lintRules;

    // Registry should only contain single lint rule.
    expect(lints, hasLength(1));
    var lint = lints.first;
    expect(lint.name, camelCaseTypesLintName);

    // Verify lint error result.
    var errors = filesErrors[testFile]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, testFile.path);
    expect(error.severity, AnalysisErrorSeverity.INFO);
    expect(error.type, AnalysisErrorType.LINT);
  }

  Future<void> test_notInAnalysisRoot() async {
    await setRoots(included: [workspaceRootPath], excluded: []);
    var otherFile = newFile('/other.dart', 'UnknownType V;');
    addTestFile('''
import '/other.dart';
void f() {
  print(V);
}
''');
    await waitForTasksFinished();
    expect(filesErrors[otherFile], isNull);
  }

  Future<void> test_overlay_dotFolder() async {
    // Files inside dotFolders should not generate error notifications even
    // if they have overlays added.
    await setRoots(included: [workspaceRootPath], excluded: []);
    addTestFile('');
    var brokenFile = newFile(
      '$testPackageRootPath/.dart_tool/broken.dart',
      'err',
    );

    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    expect(filesErrors[brokenFile], isNull);

    // Add and overlay and give chance for the file to be analyzed (if
    // it would).
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        brokenFile.path: AddContentOverlay('err'),
      }).toRequest('1', clientUriConverter: server.uriConverter),
    );
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // There should still be no errors.
    expect(filesErrors[brokenFile], isNull);
  }

  Future<void> test_overlay_newFile() async {
    // Overlays added for files that don't exist on disk should still generate
    // error notifications. Removing the overlay if the file is not on disk
    // should clear the errors.
    await setRoots(included: [workspaceRootPath], excluded: []);
    addTestFile('');
    var brokenFile = getFile('$testPackageRootPath/broken.dart');

    // Add and overlay and give chance for the file to be analyzed.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        brokenFile.path: AddContentOverlay('err'),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // There should now be errors.
    expect(filesErrors[brokenFile], hasLength(greaterThan(0)));

    // Remove the overlay (this file no longer exists anywhere).
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        brokenFile.path: RemoveContentOverlay(),
      }).toRequest('1', clientUriConverter: server.uriConverter),
    );

    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // Unlike other tests here, removing an overlay for a file that doesn't exist
    // on disk doesn't flush errors, but re-analyzes the missing file, which results
    // in an error notification of 0 errors rather than a flush.
    expect(filesErrors[brokenFile], isEmpty);
  }

  Future<void> test_overlay_newFileSavedBeforeRemoving() async {
    // Overlays added for files that don't exist on disk should still generate
    // error notifications. If the file is subsequently saved to disk before the
    // overlay is removed, the errors should not be flushed when the overlay is
    // removed.
    await setRoots(included: [workspaceRootPath], excluded: []);
    addTestFile('');
    var brokenFile = getFile('$testPackageRootPath/broken.dart');

    // Add and overlay and give chance for the file to be analyzed.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        brokenFile.path: AddContentOverlay('err'),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // There should now be errors.
    expect(filesErrors[brokenFile], hasLength(greaterThan(0)));

    // Write the file to disk.
    brokenFile.writeAsStringSync('err');
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // Remove the overlay.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        brokenFile.path: RemoveContentOverlay(),
      }).toRequest('1', clientUriConverter: server.uriConverter),
    );
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);

    // Errors should not have been flushed since the file still exists without
    // the overlay.
    expect(filesErrors[brokenFile], hasLength(greaterThan(0)));
  }

  Future<void> test_ParserError() async {
    await setRoots(included: [workspaceRootPath], excluded: []);
    addTestFile('library lib');
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    var errors = filesErrors[testFile]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, testFile.path);
    expect(error.location.offset, isPositive);
    expect(error.location.length, isNonNegative);
    expect(error.severity, AnalysisErrorSeverity.ERROR);
    expect(error.type, AnalysisErrorType.SYNTACTIC_ERROR);
    expect(error.message, isNotNull);
  }

  Future<void> test_pubspecFile() async {
    var pubspecFile = newPubspecYamlFile(testPackageRootPath, '''
version: 1.3.2
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();
    await pumpEventQueue();
    //
    // Verify the error result.
    //
    var errors = filesErrors[pubspecFile]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, pubspecFile.path);
    expect(error.severity, AnalysisErrorSeverity.WARNING);
    expect(error.type, AnalysisErrorType.STATIC_WARNING);
    //
    // Fix the error and verify the new results.
    //
    pubspecFile.writeAsStringSync('''
name: sample
version: 1.3.2
''');
    await waitForTasksFinished();
    await pumpEventQueue();

    errors = filesErrors[pubspecFile]!;
    expect(errors, hasLength(0));
  }

  Future<void> test_pubspecFile_lint() async {
    newAnalysisOptionsYamlFile(testPackageRootPath, '''
linter:
  rules:
    - sort_pub_dependencies
''');

    var pubspecFile = newPubspecYamlFile(testPackageRootPath, '''
name: sample

dependencies:
  b: any
  a: any
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();
    await pumpEventQueue();
    //
    // Verify the error result.
    //
    var errors = filesErrors[pubspecFile]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.location.file, pubspecFile.path);
    expect(error.severity, AnalysisErrorSeverity.INFO);
    expect(error.type, AnalysisErrorType.LINT);
    //
    // Fix the error and verify the new results.
    //
    pubspecFile.writeAsStringSync('''
name: sample

dependencies:
  a: any
  b: any
''');
    await waitForTasksFinished();
    await pumpEventQueue();

    errors = filesErrors[pubspecFile]!;
    expect(errors, hasLength(0));
  }

  Future<void> test_StaticWarning() async {
    await setRoots(included: [workspaceRootPath], excluded: []);
    addTestFile('''
enum E {e1, e2}

void f(E e) {
  switch (e) {
    case E.e1:
      print(0);
      break;
  }
}
''');
    await waitForTasksFinished();
    await pumpEventQueue(times: 5000);
    var errors = filesErrors[testFile]!;
    expect(errors, hasLength(1));
    var error = errors[0];
    expect(error.severity, AnalysisErrorSeverity.ERROR);
    expect(error.type, AnalysisErrorType.COMPILE_TIME_ERROR);
  }
}
