// 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/dart/analysis/features.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/context/source.dart';
import 'package:analyzer/src/dart/analysis/experiments.dart';
import 'package:analyzer/src/dart/analysis/experiments_impl.dart';
import 'package:analyzer/src/dart/analysis/feature_set_provider.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/file_system/file_system.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(FeatureSetProviderTest);
  });
}

@reflectiveTest
class FeatureSetProviderTest with ResourceProviderMixin {
  late SourceFactory sourceFactory;
  late FeatureSetProvider provider;

  final latestLanguageVersionFeatures = FeatureSet.latestLanguageVersion();

  Folder get sdkRoot => newFolder('/sdk');

  void setUp() {
    newFile('/test/lib/test.dart', '');

    createMockSdk(
      resourceProvider: resourceProvider,
      root: sdkRoot,
    );

    _createSourceFactory();
  }

  test_getFeatureSet_allowedExperiments() {
    var feature_a = ExperimentalFeature(
      index: 0,
      enableString: 'a',
      isEnabledByDefault: false,
      isExpired: false,
      documentation: 'a',
      experimentalReleaseVersion: null,
      releaseVersion: null,
    );

    _newSdkExperimentsFile(r'''
{
  "version": 1,
  "experimentSets": {
    "with_a": ["a"]
  },
  "sdk": {
    "default": {
      "experimentSet": "with_a"
    }
  },
  "packages": {
    "aaa": {
      "experimentSet": "with_a"
    }
  }
}
''');

    var packages = Packages(
      {
        'aaa': Package(
          name: 'aaa',
          rootFolder: newFolder('/packages/aaa'),
          libFolder: newFolder('/packages/aaa/lib'),
          languageVersion: null,
        ),
        'bbb': Package(
          name: 'bbb',
          rootFolder: newFolder('/packages/bbb'),
          libFolder: newFolder('/packages/bbb/lib'),
          languageVersion: null,
        ),
      },
    );

    _createSourceFactory(
      packageUriResolver: _createPackageMapUriResolver(packages),
    );

    overrideKnownFeatures({'a': feature_a}, () {
      provider = FeatureSetProvider.build(
        sourceFactory: sourceFactory,
        resourceProvider: resourceProvider,
        packages: packages,
      );

      void assertHasFeature(String path, bool expected) {
        _assertHasFeatureForPath(
          path,
          feature_a,
          expected,
          contextFeatures: ExperimentStatus(),
          nonPackageFeatureSet: ExperimentStatus(),
        );
      }

      assertHasFeature('/packages/aaa/lib/a.dart', true);
      assertHasFeature('/packages/aaa/bin/b.dart', true);
      assertHasFeature('/packages/aaa/test/c.dart', true);

      assertHasFeature('/packages/bbb/lib/a.dart', false);
      assertHasFeature('/packages/bbb/bin/b.dart', false);
      assertHasFeature('/packages/bbb/test/c.dart', false);

      assertHasFeature('/other/file.dart', false);
    });
  }

  test_getFeatureSet_defaultForContext_hasExperiment() {
    var feature_a = ExperimentalFeature(
      index: 0,
      enableString: 'a',
      isEnabledByDefault: false,
      isExpired: false,
      documentation: 'a',
      experimentalReleaseVersion: Version.parse('2.12.0'),
      releaseVersion: null,
    );

    var packages = Packages(
      {
        'aaa': Package(
          name: 'aaa',
          rootFolder: newFolder('/packages/aaa'),
          libFolder: newFolder('/packages/aaa/lib'),
          languageVersion: null,
        ),
        'bbb': Package(
          name: 'bbb',
          rootFolder: newFolder('/packages/bbb'),
          libFolder: newFolder('/packages/bbb/lib'),
          languageVersion: Version(2, 12, 0),
        ),
      },
    );

    _createSourceFactory(
      packageUriResolver: _createPackageMapUriResolver(packages),
    );

    overrideKnownFeatures({'a': feature_a}, () {
      provider = FeatureSetProvider.build(
        sourceFactory: sourceFactory,
        resourceProvider: resourceProvider,
        packages: packages,
      );

      void assertHasFeature(String path, bool expected) {
        _assertHasFeatureForPath(
          path,
          feature_a,
          expected,
          contextFeatures: FeatureSet.fromEnableFlags2(
            sdkLanguageVersion: Version.parse('2.12.0'),
            flags: [feature_a.enableString],
          ),
          nonPackageFeatureSet: latestLanguageVersionFeatures,
        );
      }

      assertHasFeature('/packages/aaa/a.dart', true);
      assertHasFeature('/packages/aaa/lib/b.dart', true);
      assertHasFeature('/packages/aaa/test/c.dart', true);

      assertHasFeature('/packages/bbb/a.dart', true);
      assertHasFeature('/packages/bbb/lib/b.dart', true);
      assertHasFeature('/packages/bbb/test/c.dart', true);
    });
  }

  test_getFeatureSet_defaultForContext_noExperiments() {
    var feature_a = ExperimentalFeature(
      index: 0,
      enableString: 'a',
      isEnabledByDefault: false,
      isExpired: false,
      documentation: 'a',
      experimentalReleaseVersion: Version.parse('2.12.0'),
      releaseVersion: null,
    );

    var packages = Packages(
      {
        'aaa': Package(
          name: 'aaa',
          rootFolder: newFolder('/packages/aaa'),
          libFolder: newFolder('/packages/aaa/lib'),
          languageVersion: null,
        ),
        'bbb': Package(
          name: 'bbb',
          rootFolder: newFolder('/packages/bbb'),
          libFolder: newFolder('/packages/bbb/lib'),
          languageVersion: Version(2, 12, 0),
        ),
      },
    );

    _createSourceFactory(
      packageUriResolver: _createPackageMapUriResolver(packages),
    );

    overrideKnownFeatures({'a': feature_a}, () {
      provider = FeatureSetProvider.build(
        sourceFactory: sourceFactory,
        resourceProvider: resourceProvider,
        packages: packages,
      );

      void assertHasFeature(String path, bool expected) {
        _assertHasFeatureForPath(
          path,
          feature_a,
          expected,
          contextFeatures: ExperimentStatus(),
          nonPackageFeatureSet: ExperimentStatus(),
        );
      }

      assertHasFeature('/packages/aaa/a.dart', false);
      assertHasFeature('/packages/aaa/lib/b.dart', false);
      assertHasFeature('/packages/aaa/test/c.dart', false);

      assertHasFeature('/packages/bbb/a.dart', false);
      assertHasFeature('/packages/bbb/lib/b.dart', false);
      assertHasFeature('/packages/bbb/test/c.dart', false);
    });
  }

  test_packages_contextExperiments_nested() {
    var packages = Packages(
      {
        'aaa': Package(
          name: 'aaa',
          rootFolder: getFolder('/packages/aaa'),
          libFolder: getFolder('/packages/aaa/lib'),
          languageVersion: Version.parse('2.5.0'),
        ),
        'bbb': Package(
          name: 'bbb',
          rootFolder: getFolder('/packages/aaa/bbb'),
          libFolder: getFolder('/packages/aaa/bbb/lib'),
          languageVersion: Version.parse('2.6.0'),
        ),
        'ccc': Package(
          name: 'ccc',
          rootFolder: getFolder('/packages/ccc'),
          libFolder: getFolder('/packages/ccc/lib'),
          languageVersion: Version.parse('2.7.0'),
        ),
      },
    );

    provider = FeatureSetProvider.build(
      sourceFactory: sourceFactory,
      resourceProvider: resourceProvider,
      packages: packages,
    );

    void check({
      required String uriStr,
      required String posixPath,
      required Version expected,
    }) {
      var uri = Uri.parse(uriStr);
      var path = convertPath(posixPath);
      expect(
        provider.getLanguageVersion(path, uri,
            nonPackageLanguageVersion: ExperimentStatus.currentVersion),
        expected,
      );
    }

    check(
      uriStr: 'package:aaa/a.dart',
      posixPath: '/packages/aaa/a.dart',
      expected: Version.parse('2.5.0'),
    );

    check(
      uriStr: toUriStr('/packages/aaa/test/a.dart'),
      posixPath: '/packages/aaa/test/a.dart',
      expected: Version.parse('2.5.0'),
    );

    check(
      uriStr: 'package:bbb/b.dart',
      posixPath: '/packages/aaa/bbb/b.dart',
      expected: Version.parse('2.6.0'),
    );

    check(
      uriStr: 'package:ccc/c.dart',
      posixPath: '/packages/ccc/c.dart',
      expected: Version.parse('2.7.0'),
    );

    check(
      uriStr: 'package:ddd/a.dart',
      posixPath: '/packages/ddd/d.dart',
      expected: ExperimentStatus.currentVersion,
    );
  }

  test_sdk_allowedExperiments_default() {
    var feature_a = ExperimentalFeature(
      index: 0,
      enableString: 'a',
      isEnabledByDefault: false,
      isExpired: false,
      documentation: 'a',
      experimentalReleaseVersion: null,
      releaseVersion: null,
    );

    _newSdkExperimentsFile(r'''
{
  "version": 1,
  "experimentSets": {
    "with_a": ["a"]
  },
  "sdk": {
    "default": {
      "experimentSet": "with_a"
    }
  }
}
''');

    overrideKnownFeatures({'a': feature_a}, () {
      provider = FeatureSetProvider.build(
        sourceFactory: sourceFactory,
        resourceProvider: resourceProvider,
        packages: findPackagesFrom(resourceProvider, getFolder('/test')),
      );

      var core_featureSet = _getSdkFeatureSet(
        'dart:core',
        contextFeatures: latestLanguageVersionFeatures,
        nonPackageFeatureSet: latestLanguageVersionFeatures,
      );
      expect(core_featureSet.isEnabled(feature_a), isTrue);

      var math_featureSet = _getSdkFeatureSet(
        'dart:math',
        contextFeatures: latestLanguageVersionFeatures,
        nonPackageFeatureSet: latestLanguageVersionFeatures,
      );
      expect(math_featureSet.isEnabled(feature_a), isTrue);
    });
  }

  test_sdk_allowedExperiments_library() {
    var feature_a = ExperimentalFeature(
      index: 0,
      enableString: 'a',
      isEnabledByDefault: false,
      isExpired: false,
      documentation: 'a',
      experimentalReleaseVersion: null,
      releaseVersion: null,
    );

    _newSdkExperimentsFile(r'''
{
  "version": 1,
  "experimentSets": {
    "none": [],
    "with_a": ["a"]
  },
  "sdk": {
    "default": {
      "experimentSet": "none"
    },
    "libraries": {
      "math": {
        "experimentSet": "with_a"
      }
    }
  }
}
''');

    overrideKnownFeatures({'a': feature_a}, () {
      provider = FeatureSetProvider.build(
        sourceFactory: sourceFactory,
        resourceProvider: resourceProvider,
        packages: findPackagesFrom(resourceProvider, getFolder('/test')),
      );

      var core_featureSet = _getSdkFeatureSet(
        'dart:core',
        contextFeatures: latestLanguageVersionFeatures,
        nonPackageFeatureSet: latestLanguageVersionFeatures,
      );
      expect(core_featureSet.isEnabled(feature_a), isFalse);

      var math_featureSet = _getSdkFeatureSet(
        'dart:math',
        contextFeatures: latestLanguageVersionFeatures,
        nonPackageFeatureSet: latestLanguageVersionFeatures,
      );
      expect(math_featureSet.isEnabled(feature_a), isTrue);
    });
  }

  test_sdk_allowedExperiments_mockDefault() {
    provider = FeatureSetProvider.build(
      sourceFactory: sourceFactory,
      resourceProvider: resourceProvider,
      packages: findPackagesFrom(resourceProvider, getFolder('/test')),
    );

    var featureSet = _getSdkFeatureSet(
      'dart:math',
      contextFeatures: latestLanguageVersionFeatures,
      nonPackageFeatureSet: latestLanguageVersionFeatures,
    );
    expect(featureSet.isEnabled(Feature.non_nullable), isTrue);
  }

  _assertHasFeatureForPath(
    String path,
    Feature feature,
    bool expected, {
    required FeatureSet contextFeatures,
    required FeatureSet nonPackageFeatureSet,
  }) {
    var featureSet = _getPathFeatureSet(
      path,
      contextFeatures: contextFeatures,
      nonPackageFeatureSet: nonPackageFeatureSet,
    );
    expect(featureSet.isEnabled(feature), expected);
  }

  PackageMapUriResolver _createPackageMapUriResolver(Packages packages) {
    var map = <String, List<Folder>>{};
    for (var package in packages.packages) {
      map[package.name] = [package.libFolder];
    }
    return PackageMapUriResolver(resourceProvider, map);
  }

  void _createSourceFactory({UriResolver? packageUriResolver}) {
    var resolvers = <UriResolver>[];
    if (packageUriResolver != null) {
      resolvers.add(packageUriResolver);
    }
    resolvers.addAll([
      DartUriResolver(
        FolderBasedDartSdk(resourceProvider, sdkRoot),
      ),
      ResourceUriResolver(resourceProvider),
    ]);
    sourceFactory = SourceFactoryImpl(resolvers);
  }

  FeatureSet _getPathFeatureSet(
    String path, {
    required FeatureSet contextFeatures,
    required FeatureSet nonPackageFeatureSet,
  }) {
    path = convertPath(path);
    var uri = sourceFactory.pathToUri(path)!;
    return provider.getFeatureSet(
      path,
      uri,
      contextFeatures: contextFeatures,
      nonPackageFeatureSet: nonPackageFeatureSet,
    );
  }

  FeatureSet _getSdkFeatureSet(
    String uriStr, {
    required FeatureSet contextFeatures,
    required FeatureSet nonPackageFeatureSet,
  }) {
    var uri = Uri.parse(uriStr);
    var path = sourceFactory.forUri2(uri)!.fullName;
    return provider.getFeatureSet(path, uri,
        contextFeatures: contextFeatures,
        nonPackageFeatureSet: nonPackageFeatureSet);
  }

  void _newSdkExperimentsFile(String content) {
    newFile('$sdkRoot/lib/_internal/allowed_experiments.json', content);
  }
}
