// Copyright (c) 2019, 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:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'initialization_test.dart';
import 'server_abstract.dart';

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

@reflectiveTest
class ChangeWorkspaceFoldersTest extends AbstractLspAnalysisServerTest {
  late String workspaceFolder1Path, workspaceFolder2Path, workspaceFolder3Path;
  late Uri workspaceFolder1Uri, workspaceFolder2Uri, workspaceFolder3Uri;
  late Uri nonFileWorkspaceFolderUri;

  @override
  void setUp() {
    super.setUp();
    workspaceFolder1Path = convertPath('/workspace1');
    workspaceFolder2Path = convertPath('/workspace2');
    workspaceFolder3Path = convertPath('/workspace3');
    newFolder(workspaceFolder1Path);
    newFolder(workspaceFolder2Path);
    newFolder(workspaceFolder3Path);

    workspaceFolder1Uri = pathContext.toUri(workspaceFolder1Path);
    workspaceFolder2Uri = pathContext.toUri(workspaceFolder2Path);
    workspaceFolder3Uri = pathContext.toUri(workspaceFolder3Path);

    newPackageConfigJsonFile(workspaceFolder1Path, '');
    newPackageConfigJsonFile(workspaceFolder2Path, '');
    newPackageConfigJsonFile(workspaceFolder3Path, '');

    nonFileWorkspaceFolderUri = Uri.parse('dart-foo:/foo.dart');
  }

  Future<void> test_changeWorkspaceFolders_add() async {
    await initialize(rootUri: workspaceFolder1Uri);
    await changeWorkspaceFolders(
      add: [workspaceFolder2Uri, workspaceFolder3Uri],
    );

    expect(
      server.contextManager.includedPaths,
      unorderedEquals([
        workspaceFolder1Path,
        workspaceFolder2Path,
        workspaceFolder3Path,
      ]),
    );
  }

  Future<void> test_changeWorkspaceFolders_addAndRemove() async {
    await initialize(
      workspaceFolders: [workspaceFolder1Uri, workspaceFolder2Uri],
    );

    await changeWorkspaceFolders(
      add: [workspaceFolder3Uri],
      remove: [workspaceFolder2Uri],
    );
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path, workspaceFolder3Path]),
    );
  }

  Future<void> test_changeWorkspaceFolders_addAndRemove_trailingSlash() async {
    await initialize(workspaceFolders: []);
    // Test:
    // - folder1: add with, remove with
    // - folder2: add with, remove without
    // - folder3: add without, remove with

    // Add and ensure they're all listed without their trailing slashes.
    await changeWorkspaceFolders(
      add: [
        withTrailingSlashUri(workspaceFolder1Uri),
        withTrailingSlashUri(workspaceFolder2Uri),
        workspaceFolder3Uri,
      ],
    );
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([
        workspaceFolder1Path,
        workspaceFolder2Path,
        workspaceFolder3Path,
      ]),
    );

    // Remove and ensure they were all removed.
    await changeWorkspaceFolders(
      remove: [
        withTrailingSlashUri(workspaceFolder1Uri),
        workspaceFolder2Uri,
        withTrailingSlashUri(workspaceFolder3Uri),
      ],
    );
    expect(server.contextManager.includedPaths, isEmpty);
  }

  Future<void>
  test_changeWorkspaceFolders_addExplicitParentOfImplicit_closeFile() async {
    var nestedFolderPath = join(
      workspaceFolder1Path,
      'nested',
      'deeply',
      'in',
      'folders',
    );
    var nestedFilePath = join(nestedFolderPath, 'test.dart');
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');

    await initialize(allowEmptyRootUri: true);
    await openFile(nestedFileUri, '');

    // Expect implicit root for the open file.
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );

    // Add the real project root to the workspace (which will become an
    // explicit root but not change anything or rebuild contexts).
    resetContextBuildCounter();
    await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();

    // Closing the file should not change roots nor trigger a rebuild.
    resetContextBuildCounter();
    await closeFile(nestedFileUri);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();
  }

  Future<void>
  test_changeWorkspaceFolders_addExplicitParentOfImplicit_closeFolder() async {
    var nestedFolderPath = join(
      workspaceFolder1Path,
      'nested',
      'deeply',
      'in',
      'folders',
    );
    var nestedFilePath = join(nestedFolderPath, 'test.dart');
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');

    await initialize(allowEmptyRootUri: true);
    await openFile(nestedFileUri, '');

    // Expect implicit root for the open file.
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );

    // Add the real project root to the workspace (which will become an
    // explicit root but not change anything or rebuild contexts).
    resetContextBuildCounter();
    await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();

    // Removing the workspace folder should not change roots nor trigger a
    // rebuild because the root is still the implicit root for the open file.
    resetContextBuildCounter();
    await closeFile(nestedFileUri);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();
  }

  Future<void>
  test_changeWorkspaceFolders_addImplicitChildOfExplicitParent_closeFile() async {
    var nestedFolderPath = join(
      workspaceFolder1Path,
      'nested',
      'deeply',
      'in',
      'folders',
    );
    var nestedFilePath = join(nestedFolderPath, 'test.dart');
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');

    await initialize(workspaceFolders: [workspaceFolder1Uri]);

    // Expect explicit root for the workspace folder.
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );

    // An open file should not trigger any changes or rebuilds.
    resetContextBuildCounter();
    await openFile(nestedFileUri, '');
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();

    // Closing the file should also not trigger any changes.
    resetContextBuildCounter();
    await closeFile(nestedFileUri);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();
  }

  Future<void>
  test_changeWorkspaceFolders_addImplicitChildOfExplicitParent_closeFolder() async {
    var nestedFolderPath = join(
      workspaceFolder1Path,
      'nested',
      'deeply',
      'in',
      'folders',
    );
    var nestedFilePath = join(nestedFolderPath, 'test.dart');
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');

    await initialize(workspaceFolders: [workspaceFolder1Uri]);

    // Expect explicit root for the workspace folder.
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );

    // Open a file, though no new root (or rebuild) is expected as it was mapped
    // to the existing open project folder.
    resetContextBuildCounter();
    await openFile(nestedFileUri, '');
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();

    // Removing the workspace folder will retain the workspace folder, as that's
    // the project root.
    resetContextBuildCounter();
    await changeWorkspaceFolders(remove: [workspaceFolder1Uri]);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();
  }

  Future<void> test_changeWorkspaceFolders_implicitFile_noProject() async {
    var nestedFolderPath = join(
      workspaceFolder1Path,
      'nested',
      'deeply',
      'in',
      'folders',
    );
    var nestedFilePath = join(nestedFolderPath, 'test.dart');
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');

    // Ensure no package config in tree.
    deleteFile(
      join(
        workspaceFolder1Path,
        file_paths.dotDartTool,
        file_paths.packageConfigJson,
      ),
    );

    await initialize(allowEmptyRootUri: true);
    await openFile(nestedFileUri, '');

    // Because there is no pubspec in the tree and we don't locate a root, we
    // expect the file to be analyzed solo.
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([nestedFilePath]),
    );

    // Adding the parent folder will switch to using that as the root and rebuild
    // the root.
    resetContextBuildCounter();
    await changeWorkspaceFolders(add: [workspaceFolder1Uri]);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectContextBuilds();
  }

  /// Verifies adding non-file workspace folders does not fail.
  ///
  /// Related tests for initial workspace folders are in [InitializationTest].
  Future<void> test_changeWorkspaceFolders_nonFile_add() async {
    await initialize(workspaceFolders: [workspaceFolder1Uri]);
    await changeWorkspaceFolders(
      add: [workspaceFolder2Uri, nonFileWorkspaceFolderUri],
    );

    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path, workspaceFolder2Path]),
    );
  }

  /// Verifies removing non-file workspace folders does not fail.
  ///
  /// Related tests for initial workspace folders are in [InitializationTest].
  Future<void> test_changeWorkspaceFolders_nonFile_remove() async {
    await initialize(
      workspaceFolders: [
        workspaceFolder1Uri,
        workspaceFolder2Uri,
        nonFileWorkspaceFolderUri,
      ],
    );
    await changeWorkspaceFolders(
      remove: [workspaceFolder2Uri, nonFileWorkspaceFolderUri],
    );

    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
  }

  Future<void> test_changeWorkspaceFolders_openFileOutsideRoot() async {
    // When a file is opened that is outside of the analysis roots, the first
    // analysis driver will be used (see [AbstractAnalysisServer.getAnalysisDriver])
    // and no new root will be created.
    var workspace1FilePath = join(workspaceFolder1Path, 'test.dart');
    newFile(workspace1FilePath, '');
    var workspace2FilePath = join(workspaceFolder2Path, 'test.dart');
    var workspace2FileUri = pathContext.toUri(workspace2FilePath);
    newFile(workspace2FilePath, '');

    await initialize(workspaceFolders: [workspaceFolder1Uri]);

    // Expect explicit root for the workspace folder.
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );

    // Open a file in workspaceFolder2 which will reuse the existing driver for
    // workspace1 so not change roots/trigger a rebuild.
    resetContextBuildCounter();
    await openFile(workspace2FileUri, '');
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();

    // Closing the file will also not trigger any changes.
    resetContextBuildCounter();
    await closeFile(workspace2FileUri);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
    expectNoContextBuilds();
  }

  Future<void> test_changeWorkspaceFolders_pluginAnalysisRoots() async {
    await initialize(workspaceFolders: [workspaceFolder1Uri]);
    expect(
      pluginManager.analysisSetAnalysisRootsParams?.included,
      unorderedEquals([workspaceFolder1Path]),
    );

    await changeWorkspaceFolders(
      add: [workspaceFolder2Uri, workspaceFolder3Uri],
    );
    expect(
      pluginManager.analysisSetAnalysisRootsParams?.included,
      unorderedEquals([
        workspaceFolder1Path,
        workspaceFolder2Path,
        workspaceFolder3Path,
      ]),
    );

    await changeWorkspaceFolders(remove: [workspaceFolder1Uri]);
    expect(
      pluginManager.analysisSetAnalysisRootsParams?.included,
      unorderedEquals([workspaceFolder2Path, workspaceFolder3Path]),
    );
  }

  Future<void> test_changeWorkspaceFolders_remove() async {
    await initialize(
      workspaceFolders: [workspaceFolder1Uri, workspaceFolder2Uri],
    );

    await changeWorkspaceFolders(remove: [workspaceFolder2Uri]);
    expect(
      server.contextManager.includedPaths,
      unorderedEquals([workspaceFolder1Path]),
    );
  }

  Future<void> test_changeWorkspaceFolders_removeFlushesDiagnostics() async {
    failTestOnErrorDiagnostic = false;

    // Add our standard test project as well as a dummy project.
    await initialize(workspaceFolders: [projectFolderUri, workspaceFolder1Uri]);

    // Generate an error in the test project.
    var firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
    newFile(mainFilePath, 'String a = 1;');
    var initialDiagnostics = await firstDiagnosticsUpdate;
    expect(initialDiagnostics, hasLength(1));

    // Ensure the error is removed if we removed the workspace folder.
    var secondDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await changeWorkspaceFolders(remove: [projectFolderUri]);
    var updatedDiagnostics = await secondDiagnosticsUpdate;
    expect(updatedDiagnostics, hasLength(0));
  }
}
