// Copyright (c) 2022, 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/file_system/memory_file_system.dart';
import 'package:dartdoc/src/dartdoc.dart';
import 'package:dartdoc/src/logging.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/package_meta.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

import 'src/test_descriptor_utils.dart' as d;
import 'src/utils.dart';

/// Exception thrown for invalid use of [DartdocTestBase]'s api.
class DartdocTestBaseFailure implements Exception {
  final String message;

  DartdocTestBaseFailure(this.message);

  @override
  String toString() => message;
}

abstract class DartdocTestBase {
  late final PackageMetaProvider packageMetaProvider;
  late final MemoryResourceProvider resourceProvider;
  late String packagePath;

  String get libraryName;

  String get placeholder => '%%__HTMLBASE_dartdoc_internal__%%';

  String get linkPrefix => '$placeholder$libraryName';

  String get dartAsyncUrlPrefix => '$dartSdkUrlPrefix/dart-async';

  String get dartCoreUrlPrefix => '$dartSdkUrlPrefix/dart-core';

  String get dartSdkUrlPrefix => 'https://api.dart.dev/stable/3.10.6';

  String get sdkConstraint => '>=3.7.0 <4.0.0';

  /// A mapping of pub dependency names to the paths.
  ///
  /// These are written out to a pubspec file using `path` keys.
  Map<String, String> get pubDependencyPaths => const {};

  List<String> get experiments => ['enhanced-parts', 'wildcard-variables'];

  bool get skipUnreachableSdkLibraries => true;

  late StringBuffer outBuffer;
  late StringBuffer errBuffer;

  @mustCallSuper
  Future<void> setUp() async {
    outBuffer = StringBuffer();
    errBuffer = StringBuffer();
    packageMetaProvider = testPackageMetaProvider;
    resourceProvider =
        packageMetaProvider.resourceProvider as MemoryResourceProvider;
    await _setUpPackage();
  }

  Future<void> _setUpPackage() async {
    var pubspec = d.buildPubspecText(
      sdkConstraint: sdkConstraint,
      dependencies: pubDependencyPaths,
    );
    String? analysisOptions;
    if (experiments.isNotEmpty) {
      analysisOptions = '''
analyzer:
  enable-experiment:${experiments.map((experiment) => '\n  - $experiment').join('')}
''';
    }
    packagePath = await d.createPackage(
      libraryName,
      pubspec: pubspec,
      analysisOptions: analysisOptions,
      resourceProvider: resourceProvider,
    );
  }

  Future<PackageGraph> bootPackageFromFiles(Iterable<d.Descriptor> files,
      {List<String> additionalArguments = const []}) async {
    var packagePathBasename =
        resourceProvider.pathContext.basename(packagePath);
    var packagePathDirname = resourceProvider.pathContext.dirname(packagePath);
    await d
        .dir(packagePathBasename, files)
        .createInMemory(resourceProvider, packagePathDirname);
    return await bootBasicPackage(
      packagePath,
      packageMetaProvider,
      additionalArguments: additionalArguments,
      skipUnreachableSdkLibraries: skipUnreachableSdkLibraries,
    );
  }

  /// Creates a single library named [libraryName], with optional preamble
  /// [libraryPreamble].  Optionally, pass [extraFiles] such as
  /// `dartdoc_options.yaml`.
  Future<Library> bootPackageWithLibrary(
    String libraryContent, {
    String libraryFilePath = 'lib/library.dart',
    String libraryPreamble = '',
    Iterable<d.Descriptor> extraFiles = const [],
    List<String> additionalArguments = const [],
  }) async {
    return (await bootPackageFromFiles([
      d.file(libraryFilePath, '''
$libraryPreamble
library $libraryName;

$libraryContent
'''),
      ...extraFiles,
    ], additionalArguments: additionalArguments))
        .libraries
        .named(libraryName);
  }

  Dartdoc buildDartdoc({
    List<String> excludeLibraries = const [],
    List<String> additionalArguments = const [],
    bool skipUnreachableSdkLibraries = true,
    bool useJson = false,
  }) {
    final dir = resourceProvider.getFolder(resourceProvider.pathContext
        .absolute(resourceProvider.pathContext.normalize(packagePath)));
    final context = generatorContextFromArgv([
      '--input',
      dir.path,
      '--output',
      path.join(packagePath, 'doc'),
      '--sdk-dir',
      packageMetaProvider.defaultSdkDir.path,
      '--exclude',
      excludeLibraries.join(','),
      '--allow-tools',
      '--no-link-to-remote',
      ...additionalArguments,
    ], packageMetaProvider);
    final packageBuilder = PubPackageBuilder(
      context,
      packageMetaProvider,
      skipUnreachableSdkLibraries: skipUnreachableSdkLibraries,
    );
    startLogging(
      isJson: useJson,
      isQuiet: true,
      showProgress: true,
      outSink: outBuffer,
      errSink: errBuffer,
    );
    return Dartdoc.fromContext(context, packageBuilder);
  }

  /// The real offset in a library generated with [bootPackageWithLibrary].
  ///
  /// When a library is written via [bootPackageWithLibrary], the test author
  /// provides `libraryContent`, which is a snippet of Dart library text.
  int realOffsetFor(int offsetInContent) =>
      '\n\nlibrary $libraryName\n\n'.length + offsetInContent;
}
