content: // Copyright 2013 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/file.dart';
  import 'package:git/git.dart';
  import 'package:platform/platform.dart';
  import 'package:yaml/yaml.dart';
  
  import 'common/core.dart';
  import 'common/package_looping_command.dart';
  import 'common/process_runner.dart';
  import 'common/repository_package.dart';
  
  /// A command to enforce README conventions across the repository.
  class ReadmeCheckCommand extends PackageLoopingCommand {
    /// Creates an instance of the README check command.
    ReadmeCheckCommand(
      Directory packagesDir, {
      ProcessRunner processRunner = const ProcessRunner(),
      Platform platform = const LocalPlatform(),
      GitDir? gitDir,
    }) : super(
            packagesDir,
            processRunner: processRunner,
            platform: platform,
            gitDir: gitDir,
          ) {
      argParser.addFlag(_requireExcerptsArg,
          help: 'Require that Dart code blocks be managed by code-excerpt.');
    }
  
    static const String _requireExcerptsArg = 'require-excerpts';
  
    // Standardized capitalizations for platforms that a plugin can support.
    static const Map<String, String> _standardPlatformNames = <String, String>{
      'android': 'Android',
      'ios': 'iOS',
      'linux': 'Linux',
      'macos': 'macOS',
      'web': 'Web',
      'windows': 'Windows',
    };
  
    @override
    final String name = 'readme-check';
  
    @override
    final String description =
        'Checks that READMEs follow repository conventions.';
  
    @override
    bool get hasLongOutput => false;
  
    @override
    Future<PackageResult> runForPackage(RepositoryPackage package) async {
      final List<String> errors = _validateReadme(package.readmeFile,
          mainPackage: package, isExample: false);
      for (final RepositoryPackage packageToCheck in package.getExamples()) {
        errors.addAll(_validateReadme(packageToCheck.readmeFile,
            mainPackage: package, isExample: true));
      }
  
      // If there's an example/README.md for a multi-example package, validate
      // that as well, as it will be shown on pub.dev.
      final Directory exampleDir = package.directory.childDirectory('example');
      final File exampleDirReadme = exampleDir.childFile('README.md');
      if (exampleDir.existsSync() && !isPackage(exampleDir)) {
        errors.addAll(_validateReadme(exampleDirReadme,
            mainPackage: package, isExample: true));
      }
  
      return errors.isEmpty
          ? PackageResult.success()
          : PackageResult.fail(errors);
    }
  
    List<String> _validateReadme(File readme,
        {required RepositoryPackage mainPackage, required bool isExample}) {
      if (!readme.existsSync()) {
        if (isExample) {
          print('${indentation}No README for '
              '${getRelativePosixPath(readme.parent, from: mainPackage.directory)}');
          return <String>[];
        } else {
          printError('${indentation}No README found at '
              '${getRelativePosixPath(readme, from: mainPackage.directory)}');
          return <String>['Missing README.md'];
        }
      }
  
      print('${indentation}Checking '
          '${getRelativePosixPath(readme, from: mainPackage.directory)}...');
  
      final List<String> readmeLines = readme.readAsLinesSync();
      final List<String> errors = <String>[];
  
      final String? blockValidationError = _validateCodeBlocks(readmeLines);
      if (blockValidationError != null) {
        errors.add(blockValidationError);
      }
  
      if (_containsTemplateBoilerplate(readmeLines)) {
        printError('${indentation}The boilerplate section about getting started '
            'with Flutter should not be left in.');
        errors.add('Contains template boilerplate');
      }
  
      // Check if this is the main readme for a plugin, and if so enforce extra
      // checks.
      if (!isExample) {
        final Pubspec pubspec = mainPackage.parsePubspec();
        final bool isPlugin = pubspec.flutter?['plugin'] != null;
        if (isPlugin && (!mainPackage.isFederated || mainPackage.isAppFacing)) {
          final String? error = _validateSupportedPlatforms(readmeLines, pubspec);
          if (error != null) {
            errors.add(error);
          }
        }
      }
  
      return errors;
    }
  
    /// Validates that code blocks (``` ... ```) follow repository standards.
    String? _validateCodeBlocks(List<String> readmeLines) {
      final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*');
      final List<int> missingLanguageLines = <int>[];
      final List<int> missingExcerptLines = <int>[];
      bool inBlock = false;
      for (int i = 0; i < readmeLines.length; ++i) {
        final RegExpMatch? match =
            codeBlockDelimiterPattern.firstMatch(readmeLines[i]);
        if (match == null) {
          continue;
        }
        if (inBlock) {
          inBlock = false;
          continue;
        }
        inBlock = true;
  
        final int humanReadableLineNumber = i + 1;
  
        // Ensure that there's a language tag.
        final String infoString = match[1] ?? '';
        if (infoString.isEmpty) {
          missingLanguageLines.add(humanReadableLineNumber);
          continue;
        }
  
        // Check for code-excerpt usage if requested.
        if (getBoolArg(_requireExcerptsArg) && infoString == 'dart') {
          const String excerptTagStart = '<?code-excerpt ';
          if (i == 0 || !readmeLines[i - 1].trim().startsWith(excerptTagStart)) {
            missingExcerptLines.add(humanReadableLineNumber);
          }
        }
      }
  
      String? errorSummary;
  
      if (missingLanguageLines.isNotEmpty) {
        for (final int lineNumber in missingLanguageLines) {
          printError('${indentation}Code block at line $lineNumber is missing '
              'a language identifier.');
        }
        printError(
            '\n${indentation}For each block listed above, add a language tag to '
            'the opening block. For instance, for Dart code, use:\n'
            '${indentation * 2}```dart\n');
        errorSummary = 'Missing language identifier for code block';
      }
  
      if (missingExcerptLines.isNotEmpty) {
        for (final int lineNumber in missingExcerptLines) {
          printError('${indentation}Dart code block at line $lineNumber is not '
              'managed by code-excerpt.');
        }
        printError(
            '\n${indentation}For each block listed above, add <?code-excerpt ...> '
            'tag on the previous line, and ensure that a build.excerpt.yaml is '
            'configured for the source example.\n');
        errorSummary ??= 'Missing code-excerpt management for code block';
      }
  
      return errorSummary;
    }
  
    /// Validates that the plugin has a supported platforms table following the
    /// expected format, returning an error string if any issues are found.
    String? _validateSupportedPlatforms(
        List<String> readmeLines, Pubspec pubspec) {
      // Example table following expected format:
      // |                | Android | iOS      | Web                    |
      // |----------------|---------|----------|------------------------|
      // | **Support**    | SDK 21+ | iOS 10+* | [See `camera_web `][1] |
      final int detailsLineNumber = readmeLines
          .indexWhere((String line) => line.startsWith('| **Support**'));
      if (detailsLineNumber == -1) {
        return 'No OS support table found';
      }
      final int osLineNumber = detailsLineNumber - 2;
      if (osLineNumber < 0 || !readmeLines[osLineNumber].startsWith('|')) {
        return 'OS support table does not have the expected header format';
      }
  
      // Utility method to convert an iterable of strings to a case-insensitive
      // sorted, comma-separated string of its elements.
      String sortedListString(Iterable<String> entries) {
        final List<String> entryList = entries.toList();
        entryList.sort(
            (String a, String b) => a.toLowerCase().compareTo(b.toLowerCase()));
        return entryList.join(', ');
      }
  
      // Validate that the supported OS lists match.
      final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms'];
      if (platformsEntry == null) {
        logWarning('Plugin not support any platforms');
        return null;
      }
      final YamlMap platformSupportMaps = platformsEntry as YamlMap;
      final Set<String> actuallySupportedPlatform =
          platformSupportMaps.keys.toSet().cast<String>();
      final Iterable<String> documentedPlatforms = readmeLines[osLineNumber]
          .split('|')
          .map((String entry) => entry.trim())
          .where((String entry) => entry.isNotEmpty);
      final Set<String> documentedPlatformsLowercase =
          documentedPlatforms.map((String entry) => entry.toLowerCase()).toSet();
      if (actuallySupportedPlatform.length != documentedPlatforms.length ||
          actuallySupportedPlatform
                  .intersection(documentedPlatformsLowercase)
                  .length !=
              actuallySupportedPlatform.length) {
        printError('''
  ${indentation}OS support table does not match supported platforms:
  ${indentation * 2}Actual:     ${sortedListString(actuallySupportedPlatform)}
  ${indentation * 2}Documented: ${sortedListString(documentedPlatformsLowercase)}
  ''');
        return 'Incorrect OS support table';
      }
  
      // Enforce a standard set of capitalizations for the OS headings.
      final Iterable<String> incorrectCapitalizations = documentedPlatforms
          .toSet()
          .difference(_standardPlatformNames.values.toSet());
      if (incorrectCapitalizations.isNotEmpty) {
        final Iterable<String> expectedVersions = incorrectCapitalizations
            .map((String name) => _standardPlatformNames[name.toLowerCase()]!);
        printError('''
  ${indentation}Incorrect OS capitalization: ${sortedListString(incorrectCapitalizations)}
  ${indentation * 2}Please use standard capitalizations: ${sortedListString(expectedVersions)}
  ''');
        return 'Incorrect OS support formatting';
      }
  
      // TODO(stuartmorgan): Add validation that the minimums in the table are
      // consistent with what the current implementations require. See
      // https://github.com/flutter/flutter/issues/84200
      return null;
    }
  
    /// Returns true if the README still has the boilerplate from the
    /// `flutter create` templates.
    bool _containsTemplateBoilerplate(List<String> readmeLines) =>
        readmeLines.any((String line) =>
            line.contains('For help getting started with Flutter'));
  }
  