// Copyright 2014 The Flutter Authors. 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:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_features.dart';
import 'package:flutter_tools/src/flutter_features_config.dart';

import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';

void main() {
  group('Features', () {
    testWithoutContext('setting has safe defaults', () {
      const featureSetting = FeatureChannelSetting();

      expect(featureSetting.available, false);
      expect(featureSetting.enabledByDefault, false);
    });

    testWithoutContext('has safe defaults', () {
      const feature = Feature(name: 'example');

      expect(feature.name, 'example');
      expect(feature.environmentOverride, null);
      expect(feature.configSetting, null);
    });

    testWithoutContext('retrieves the correct setting for each branch', () {
      const masterSetting = FeatureChannelSetting(available: true);
      const betaSetting = FeatureChannelSetting(available: true);
      const stableSetting = FeatureChannelSetting(available: true);
      const feature = Feature(
        name: 'example',
        master: masterSetting,
        beta: betaSetting,
        stable: stableSetting,
      );

      expect(feature.getSettingForChannel('master'), masterSetting);
      expect(feature.getSettingForChannel('beta'), betaSetting);
      expect(feature.getSettingForChannel('stable'), stableSetting);
      expect(feature.getSettingForChannel('unknown'), masterSetting);
    });

    testWithoutContext('reads from configuration if available', () {
      const exampleFeature = Feature(
        name: 'example',
        master: FeatureChannelSetting(available: true),
      );

      final flags = FlutterFeatureFlags(
        flutterVersion: FakeFlutterVersion(),
        featuresConfig: _FakeFeaturesConfig()..cannedResponse[exampleFeature] = true,
        platform: FakePlatform(),
      );
      expect(flags.isEnabled(exampleFeature), true);
    });

    testWithoutContext('returns false if not available', () {
      const exampleFeature = Feature(name: 'example');

      final flags = FlutterFeatureFlags(
        flutterVersion: FakeFlutterVersion(),
        featuresConfig: _FakeFeaturesConfig()..cannedResponse[exampleFeature] = true,
        platform: FakePlatform(),
      );
      expect(flags.isEnabled(exampleFeature), false);
    });

    FileSystem createFsWithPubspec() {
      final FileSystem fs = MemoryFileSystem.test();
      fs.currentDirectory.childFile('pubspec.yaml').writeAsStringSync('''
        flutter:
          config:
            enable-foo: true
            enable-bar: false
            enable-baz: true
      ''');
      return fs;
    }

    testUsingContext(
      'FeatureFlags is influenced by the CWD',
      () {
        // This test intentionally uses Context, as featureFlags is read that way at runtime.
        final FeatureFlags featureFlagsFromContext = featureFlags;

        // Try a few flags that don't actually exist, but we want to check configuration more e2e-y.
        expect(
          featureFlagsFromContext.isEnabled(
            const Feature(
              name: 'foo',
              configSetting: 'enable-foo',
              master: FeatureChannelSetting(available: true),
            ),
          ),
          isTrue,
          reason: 'enable-foo: true is in pubspec.yaml',
        );

        expect(
          featureFlagsFromContext.isEnabled(
            const Feature.fullyEnabled(name: 'bar', configSetting: 'enable-bar'),
          ),
          isFalse,
          reason: 'enable-bar: false is in pubspec.yaml',
        );

        expect(
          featureFlagsFromContext.isEnabled(
            const Feature(name: 'baz', configSetting: 'enable-baz'),
          ),
          isFalse,
          reason: 'Is not available',
        );
      },
      overrides: <Type, Generator>{
        ProcessManager: FakeProcessManager.empty,
        FileSystem: createFsWithPubspec,
      },
    );

    testUsingContext('Test feature flags match feature flags', () {
      final FeatureFlags testFeatureFlags = TestFeatureFlags();

      expect(featureFlags.allFeatures.length, equals(testFeatureFlags.allFeatures.length));

      final List<String> featureNames = featureFlags.allFeatures
          .map((Feature feature) => feature.name)
          .toList();
      final List<String> testFeatureNames = testFeatureFlags.allFeatures
          .map((Feature feature) => feature.name)
          .toList();

      expect(featureNames, unorderedEquals(testFeatureNames));
    });

    testUsingContext('Feature runtime IDs are valid', () {
      // Verify features' runtime IDs can be encoded into a Dart define.
      final runtimeIdPattern = RegExp(r'^[a-zA-Z_]+$');
      assert(runtimeIdPattern.hasMatch('multi_window'));
      assert(!runtimeIdPattern.hasMatch('multi-window'));

      final Iterable<String> runtimeIds = featureFlags.allFeatures
          .map((Feature feature) => feature.runtimeId)
          .nonNulls;

      expect(
        runtimeIds,
        everyElement(matches(runtimeIdPattern)),
        reason: 'Feature runtime ID must contain only alphabetical or underscore characters',
      );
    });
  });

  group('Linux Destkop', () {
    testUsingContext('is fully enabled', () {
      expect(flutterLinuxDesktopFeature, _isFullyEnabled);
      expect(featureFlags.allEnabledFeatures, contains(flutterLinuxDesktopFeature));
    });

    test('can be configured', () {
      expect(flutterLinuxDesktopFeature.configSetting, 'enable-linux-desktop');
      expect(flutterLinuxDesktopFeature.environmentOverride, 'FLUTTER_LINUX');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterLinuxDesktopFeature);
      expect(checkFlags.isLinuxEnabled, isTrue);
    });
  });

  group('MacOS Desktop', () {
    testUsingContext('is fully enabled', () {
      expect(flutterMacOSDesktopFeature, _isFullyEnabled);
      expect(featureFlags.allEnabledFeatures, contains(flutterMacOSDesktopFeature));
    });

    test('can be configured', () {
      expect(flutterMacOSDesktopFeature.configSetting, 'enable-macos-desktop');
      expect(flutterMacOSDesktopFeature.environmentOverride, 'FLUTTER_MACOS');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterMacOSDesktopFeature);
      expect(checkFlags.isMacOSEnabled, isTrue);
    });
  });

  group('Windows Desktop', () {
    testUsingContext('is fully enabled', () {
      expect(flutterWindowsDesktopFeature, _isFullyEnabled);
      expect(featureFlags.allEnabledFeatures, contains(flutterWindowsDesktopFeature));
    });

    test('can be configured', () {
      expect(flutterWindowsDesktopFeature.configSetting, 'enable-windows-desktop');
      expect(flutterWindowsDesktopFeature.environmentOverride, 'FLUTTER_WINDOWS');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterWindowsDesktopFeature);
      expect(checkFlags.isWindowsEnabled, isTrue);
    });
  });

  group('Web', () {
    testUsingContext('is fully enabled', () {
      expect(flutterWebFeature, _isFullyEnabled);
      expect(featureFlags.allEnabledFeatures, contains(flutterWebFeature));
    });

    test('can be configured', () {
      expect(flutterWebFeature.configSetting, 'enable-web');
      expect(flutterWebFeature.environmentOverride, 'FLUTTER_WEB');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterWebFeature);
      expect(checkFlags.isWebEnabled, isTrue);
    });
  });

  group('Android', () {
    testUsingContext('is fully enabled', () {
      expect(flutterAndroidFeature, _isFullyEnabled);
      expect(featureFlags.allEnabledFeatures, contains(flutterAndroidFeature));
    });

    test('can be configured', () {
      expect(flutterAndroidFeature.configSetting, 'enable-android');
      expect(flutterAndroidFeature.environmentOverride, isNull);
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterAndroidFeature);
      expect(checkFlags.isAndroidEnabled, isTrue);
    });
  });

  group('iOS', () {
    testUsingContext('is fully enabled', () {
      expect(flutterIOSFeature, _isFullyEnabled);
      expect(featureFlags.allEnabledFeatures, contains(flutterIOSFeature));
    });

    test('can be configured', () {
      expect(flutterIOSFeature.configSetting, 'enable-ios');
      expect(flutterIOSFeature.environmentOverride, isNull);
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterIOSFeature);
      expect(checkFlags.isIOSEnabled, isTrue);
    });
  });

  group('Fuchsia', () {
    test('is only available on master', () {
      expect(
        flutterFuchsiaFeature,
        allOf(<Matcher>[
          _onChannelIs('master', available: true, enabledByDefault: false),
          _onChannelIs('stable', available: false, enabledByDefault: false),
          _onChannelIs('beta', available: false, enabledByDefault: false),
        ]),
      );
    });

    test('can be configured', () {
      expect(flutterFuchsiaFeature.configSetting, 'enable-fuchsia');
      expect(flutterFuchsiaFeature.environmentOverride, 'FLUTTER_FUCHSIA');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterFuchsiaFeature);
      expect(checkFlags.isFuchsiaEnabled, isTrue);
    });
  });

  group('Custom Devices', () {
    test('is always available but not enabled by default', () {
      expect(
        flutterCustomDevicesFeature,
        allOf(<Matcher>[
          _onChannelIs('master', available: true, enabledByDefault: false),
          _onChannelIs('stable', available: true, enabledByDefault: false),
          _onChannelIs('beta', available: true, enabledByDefault: false),
        ]),
      );
    });

    test('can be configured', () {
      expect(flutterCustomDevicesFeature.configSetting, 'enable-custom-devices');
      expect(flutterCustomDevicesFeature.environmentOverride, 'FLUTTER_CUSTOM_DEVICES');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: flutterCustomDevicesFeature);
      expect(checkFlags.areCustomDevicesEnabled, isTrue);
    });
  });

  group('CLI Animations', () {
    testUsingContext('is always enabled', () {
      expect(cliAnimation, _isFullyEnabled);
      expect(featureFlags.allEnabledFeatures, contains(cliAnimation));
    });

    test('can be disabled by TERM=dumb', () {
      final features = FlutterFeatureFlags(
        flutterVersion: FakeFlutterVersion(),
        featuresConfig: _FakeFeaturesConfig(),
        platform: FakePlatform(environment: <String, String>{'TERM': 'dumb'}),
      );

      expect(features.isCliAnimationEnabled, isFalse);
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: cliAnimation);
      expect(checkFlags.isCliAnimationEnabled, isTrue);
    });
  });

  group('Native Assets', () {
    test('is available on master', () {
      expect(
        nativeAssets,
        allOf(<Matcher>[
          _onChannelIs('master', available: true, enabledByDefault: true),
          _onChannelIs('stable', available: false, enabledByDefault: false),
          _onChannelIs('beta', available: true, enabledByDefault: true),
        ]),
      );
    });

    test('can be configured', () {
      expect(nativeAssets.configSetting, 'enable-native-assets');
      expect(nativeAssets.environmentOverride, 'FLUTTER_NATIVE_ASSETS');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: nativeAssets);
      expect(checkFlags.isNativeAssetsEnabled, isTrue);
    });
  });

  group('Swift Package Manager', () {
    test('is available on all channels', () {
      expect(
        swiftPackageManager,
        allOf(<Matcher>[
          _onChannelIs('master', available: true, enabledByDefault: false),
          _onChannelIs('stable', available: true, enabledByDefault: false),
          _onChannelIs('beta', available: true, enabledByDefault: false),
        ]),
      );
    });

    test('can be configured', () {
      expect(swiftPackageManager.configSetting, 'enable-swift-package-manager');
      expect(swiftPackageManager.environmentOverride, 'FLUTTER_SWIFT_PACKAGE_MANAGER');
    });

    test('forwards to isEnabled', () {
      final checkFlags = _TestIsGetterForwarding(shouldInvoke: swiftPackageManager);
      expect(checkFlags.isSwiftPackageManagerEnabled, isTrue);
    });
  });
}

final class _FakeFeaturesConfig implements FlutterFeaturesConfig {
  final cannedResponse = <Feature, bool?>{};

  @override
  bool? isEnabled(Feature feature) => cannedResponse[feature];
}

Matcher _onChannelIs(String channel, {required bool available, required bool enabledByDefault}) {
  return _FeaturesMatcher(
    channel: channel,
    available: available,
    enabledByDefault: enabledByDefault,
  );
}

Matcher get _isFullyEnabled {
  return allOf(const <_FeaturesMatcher>[
    _FeaturesMatcher(channel: 'master', available: true, enabledByDefault: true),
    _FeaturesMatcher(channel: 'stable', available: true, enabledByDefault: true),
    _FeaturesMatcher(channel: 'beta', available: true, enabledByDefault: true),
  ]);
}

final class _FeaturesMatcher extends Matcher {
  const _FeaturesMatcher({
    required this.channel,
    required this.available,
    required this.enabledByDefault,
  });

  final String channel;
  final bool available;
  final bool enabledByDefault;

  @override
  Description describe(Description description) {
    description = description.add('feature on the "$channel" channel ');
    if (available) {
      description = description.add('is available ');
    } else {
      description = description.add('is not available');
    }
    description = description.add('and is ');
    if (enabledByDefault) {
      description = description.add('is enabled by default');
    } else {
      description = description.add('is not enabled by default');
    }
    return description;
  }

  @override
  bool matches(Object? item, Map<Object?, Object?> matchState) {
    if (item is! Feature) {
      return false;
    }
    final FeatureChannelSetting setting = switch (channel) {
      'master' => item.master,
      'stable' => item.stable,
      'beta' => item.beta,
      _ => throw StateError('Invalid channel: "$channel"'),
    };
    if (setting.available != available) {
      return false;
    }
    if (setting.enabledByDefault != enabledByDefault) {
      return false;
    }
    return true;
  }
}

final class _TestIsGetterForwarding with FlutterFeatureFlagsIsEnabled {
  _TestIsGetterForwarding({required this.shouldInvoke});

  final Feature shouldInvoke;
  @override
  final Platform platform = FakePlatform();

  @override
  bool isEnabled(Feature feature) {
    return feature == shouldInvoke;
  }

  @override
  List<Feature> get allFeatures => throw UnimplementedError();

  @override
  List<Feature> get allConfigurableFeatures => throw UnimplementedError();

  @override
  Iterable<Feature> get allEnabledFeatures => throw UnimplementedError();
}
