// 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.
// ignore_for_file: non_constant_identifier_names

/// This test library handles checks against the model for configurations
/// that require different PackageGraph configurations.  Since those
/// take a long time to initialize, isolate them here to keep model_test
/// fast.
library;

import 'package:async/async.dart';
import 'package:dartdoc/src/element_type.dart';
import 'package:dartdoc/src/matching_link_result.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/model_utils.dart';
import 'package:dartdoc/src/package_meta.dart';
import 'package:html/parser.dart' as html;
import 'package:test/test.dart';

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

final _testPackageGraphGinormousMemo = AsyncMemoizer<PackageGraph>();
Future<PackageGraph> get _testPackageGraphGinormous =>
    _testPackageGraphGinormousMemo.runOnce(() => utils.bootBasicPackage(
          'testing/test_package',
          pubPackageMetaProvider,
          excludeLibraries: ['css', 'code_in_commnets', 'excluded'],
          additionalArguments: [
            '--auto-include-dependencies',
            '--no-link-to-remote'
          ],
        ));

final _testPackageGraphSdkMemo = AsyncMemoizer<PackageGraph>();
Future<PackageGraph> get _testPackageGraphSdk =>
    _testPackageGraphSdkMemo.runOnce(_bootSdkPackage);

Future<PackageGraph> _bootSdkPackage() async {
  return PubPackageBuilder(
    await utils.contextFromArgv(
        ['--input', pubPackageMetaProvider.defaultSdkDir.path],
        pubPackageMetaProvider),
    pubPackageMetaProvider,
  ).buildPackageGraph();
}

void main() {
  // Experimental features not yet enabled by default.  Move tests out of this
  // block when the feature is enabled by default.
  group('Experiments', () {});

  group('constructor-tearoffs', () {
    late final Library constructorTearoffs;
    late final Class A, B, C, D, E, F;
    late final Mixin M;
    late final Typedef At, Bt, Ct, Et, Ft, NotAClass;
    late final Constructor Anew, Bnew, Cnew, Dnew, Enew, Fnew;

    setUpAll(() async {
      constructorTearoffs = (await _testPackageGraphGinormous)
          .libraries
          .named('constructor_tearoffs');
      A = constructorTearoffs.classes.named('A');
      B = constructorTearoffs.classes.named('B');
      C = constructorTearoffs.classes.named('C');
      D = constructorTearoffs.classes.named('D');
      E = constructorTearoffs.classes.named('E');
      F = constructorTearoffs.classes.named('F');
      M = constructorTearoffs.mixins.named('M');
      At = constructorTearoffs.typedefs.named('At');
      Bt = constructorTearoffs.typedefs.named('Bt');
      Ct = constructorTearoffs.typedefs.named('Ct');
      Et = constructorTearoffs.typedefs.named('Et');
      Ft = constructorTearoffs.typedefs.named('Ft');
      NotAClass = constructorTearoffs.typedefs.named('NotAClass');
      Anew = A.constructors.firstWhere((c) => c.isUnnamedConstructor);
      Bnew = B.constructors.firstWhere((c) => c.isUnnamedConstructor);
      Cnew = C.constructors.firstWhere((c) => c.isUnnamedConstructor);
      Dnew = D.constructors.firstWhere((c) => c.isUnnamedConstructor);
      Enew = E.constructors.firstWhere((c) => c.isUnnamedConstructor);
      Fnew = F.constructors.firstWhere((c) => c.isUnnamedConstructor);
    });

    test('smoke test', () {
      expect(A, isNotNull);
      expect(B, isNotNull);
      expect(C, isNotNull);
      expect(D, isNotNull);
      expect(E, isNotNull);
      expect(F, isNotNull);
      expect(M, isNotNull);
      expect(At, isNotNull);
      expect(Bt, isNotNull);
      expect(Ct, isNotNull);
      expect(Et, isNotNull);
      expect(Ft, isNotNull);
      expect(NotAClass, isNotNull);
      expect(Anew, isNotNull);
      expect(Bnew, isNotNull);
      expect(Cnew, isNotNull);
      expect(Dnew, isNotNull);
      expect(Enew, isNotNull);
      expect(Fnew, isNotNull);
    });

    test('reference regression', () {
      expect(referenceLookup(constructorTearoffs, 'A()'),
          equals(MatchingLinkResult(Anew)));
      expect(referenceLookup(constructorTearoffs, 'B()'),
          equals(MatchingLinkResult(Bnew)));
      expect(referenceLookup(constructorTearoffs, 'C()'),
          equals(MatchingLinkResult(Cnew)));
      expect(referenceLookup(constructorTearoffs, 'D()'),
          equals(MatchingLinkResult(Dnew)));
      expect(referenceLookup(constructorTearoffs, 'E()'),
          equals(MatchingLinkResult(Enew)));
      expect(referenceLookup(constructorTearoffs, 'F()'),
          equals(MatchingLinkResult(Fnew)));
    });

    test('.new works on classes', () {
      expect(referenceLookup(constructorTearoffs, 'A.new'),
          equals(MatchingLinkResult(Anew)));
      expect(referenceLookup(constructorTearoffs, 'B.new'),
          equals(MatchingLinkResult(Bnew)));
      expect(referenceLookup(constructorTearoffs, 'C.new'),
          equals(MatchingLinkResult(Cnew)));
      expect(referenceLookup(constructorTearoffs, 'D.new'),
          equals(MatchingLinkResult(Dnew)));
      expect(referenceLookup(constructorTearoffs, 'E.new'),
          equals(MatchingLinkResult(Enew)));
      expect(referenceLookup(constructorTearoffs, 'F.new'),
          equals(MatchingLinkResult(Fnew)));
    });

    test('.new works on typedefs', () {
      expect(referenceLookup(constructorTearoffs, 'At.new'),
          equals(MatchingLinkResult(Anew)));
      expect(referenceLookup(constructorTearoffs, 'Bt.new'),
          equals(MatchingLinkResult(Bnew)));
      expect(referenceLookup(constructorTearoffs, 'Ct.new'),
          equals(MatchingLinkResult(Cnew)));
      expect(referenceLookup(constructorTearoffs, 'Dt.new'),
          equals(MatchingLinkResult(Dnew)));
      expect(referenceLookup(constructorTearoffs, 'Et.new'),
          equals(MatchingLinkResult(Enew)));
      expect(referenceLookup(constructorTearoffs, 'Fstring.new'),
          equals(MatchingLinkResult(Fnew)));
      expect(referenceLookup(constructorTearoffs, 'Ft.new'),
          equals(MatchingLinkResult(Fnew)));
    });

    test('we can use (ignored) type parameters in references', () {
      expect(referenceLookup(E, 'D<String>.new'),
          equals(MatchingLinkResult(Dnew)));
      expect(referenceLookup(constructorTearoffs, 'F<T>.new'),
          equals(MatchingLinkResult(Fnew)));
      expect(
          referenceLookup(
              constructorTearoffs, 'F<InvalidThings, DoNotMatterHere>.new'),
          equals(MatchingLinkResult(Fnew)));
    });

    test('negative tests', () {
      // Mixins do not have constructors.
      expect(referenceLookup(constructorTearoffs, 'M.new'),
          equals(MatchingLinkResult(null)));
      // These things aren't expressions, parentheses are still illegal.
      expect(referenceLookup(constructorTearoffs, '(C).new'),
          equals(MatchingLinkResult(null)));

      // A bare new will still not work to reference constructors.
      // TODO(jcollins-g): reconsider this if we remove "new" as a hint.
      expect(referenceLookup(A, 'new'), equals(MatchingLinkResult(null)));
      expect(referenceLookup(At, 'new'), equals(MatchingLinkResult(null)));
    });
  });

  group('HTML is sanitized when enabled', () {
    Class classWithHtml;
    late final Method blockHtml;
    late final Method inlineHtml;
    late final PackageGraph packageGraph;
    late final Library exLibrary;

    setUpAll(() async {
      packageGraph = await utils.bootBasicPackage(
        'testing/test_package_sanitize_html',
        pubPackageMetaProvider,
        excludeLibraries: ['css', 'code_in_comments', 'excluded'],
        additionalArguments: ['--sanitize-html'],
      );

      exLibrary = packageGraph.libraries.named('ex');

      classWithHtml = exLibrary.classes.named('SanitizableHtml');
      blockHtml = classWithHtml.instanceMethods.named('blockHtml');
      inlineHtml = classWithHtml.instanceMethods.named('inlineHtml');
      for (var modelElement in packageGraph.localPublicLibraries
          .expand((l) => l.allModelElements)) {
        // Accessing this getter triggers documentation-processing.
        modelElement.documentation;
      }
    });

    test('can have auto-generated id attributes on headings', () {
      final dom = html.parseFragment(exLibrary.documentationAsHtml);
      expect(dom.querySelector('h1[id="heading-with-id"]'), isNotNull);
    });

    test('can have id attributes on headings', () {
      final dom = html.parseFragment(exLibrary.documentationAsHtml);
      expect(dom.querySelector('h1[id="my-id"]'), isNotNull);
    });

    test('cannot have capital id attributes on headings', () {
      final dom = html.parseFragment(exLibrary.documentationAsHtml);
      expect(dom.querySelector('h1[id="MY-ID"]'), isNull);
    });

    test('can have inline HTML', () {
      expect(inlineHtml.documentationAsHtml, contains('<small>'));
    });

    test('can have block HTML', () {
      expect(blockHtml.documentationAsHtml, contains('<h1>'));
    });

    test('will add rel=ugc for outgoing links', () {
      expect(inlineHtml.documentationAsHtml,
          contains('<a href="https://example.com" rel="ugc">'));
    });

    test('removes bad inline HTML', () {
      expect(inlineHtml.documentationAsHtml, isNot(contains('bad-idea')));
    });

    test('removes bad block HTML', () {
      expect(blockHtml.documentationAsHtml, isNot(contains('bad-idea')));
    });
  });

  group('HTML Injection when allowed', () {
    Class htmlInjection;
    late final Method injectSimpleHtml;
    late final Method injectHtmlFromTool;

    PackageGraph injectionPackageGraph;
    late final Library injectionExLibrary;

    setUpAll(() async {
      injectionPackageGraph = await utils.bootBasicPackage(
          'testing/test_package', pubPackageMetaProvider,
          excludeLibraries: ['css', 'code_in_comments', 'excluded'],
          additionalArguments: ['--inject-html']);

      injectionExLibrary = injectionPackageGraph.libraries.named('ex');

      htmlInjection = injectionExLibrary.classes.named('HtmlInjection');
      injectSimpleHtml =
          htmlInjection.instanceMethods.named('injectSimpleHtml');
      injectHtmlFromTool =
          htmlInjection.instanceMethods.named('injectHtmlFromTool');
      for (var modelElement in injectionPackageGraph.localPublicLibraries
          .expand((l) => l.allModelElements)) {
        // Accessing this getter triggers documentation-processing.
        modelElement.documentation;
      }
    });

    test('can inject HTML', () {
      expect(injectSimpleHtml.documentationAsHtml,
          contains('   <div style="opacity: 0.5;">[HtmlInjection]</div>'));
    });

    test('can inject HTML from tool', () {
      var envLine = RegExp(r'^Env: \{', multiLine: true);
      expect(envLine.allMatches(injectHtmlFromTool.documentation), hasLength(2),
          reason:
              '"${injectHtmlFromTool.documentation}" has wrong number of instances of "Env: {"');
      var argLine = RegExp(r'^Args: \[', multiLine: true);
      expect(
          argLine.allMatches(injectHtmlFromTool.documentation), hasLength(2));
      expect(
          injectHtmlFromTool.documentation,
          contains(
              'Invokes more than one tool in the same comment block, and injects HTML.'));
      expect(injectHtmlFromTool.documentationAsHtml,
          contains('<div class="title">Title</div>'));
      expect(injectHtmlFromTool.documentationAsHtml,
          isNot(contains('{@inject-html}')));
      expect(injectHtmlFromTool.documentationAsHtml,
          isNot(contains('{@end-inject-html}')));
    });
    test('tool outputs a macro which outputs injected HTML', () {
      var ToolPrintingMacroWhichInjectsHtml =
          injectionExLibrary.classes.named('ToolPrintingMacroWhichInjectsHtml');
      var a = ToolPrintingMacroWhichInjectsHtml.instanceFields.named('a');
      expect(a.documentationAsHtml,
          contains('<p>Text.</p>\n<p><div class="title">Title</div></p>'));
      var b = ToolPrintingMacroWhichInjectsHtml.instanceFields.named('b');
      expect(b.documentationAsHtml,
          contains('<p>Text.</p>\n<p><div class="title">Title</div></p>'));
    });
  });

  group('Missing and Remote', () {
    late final PackageGraph ginormousPackageGraph;

    setUpAll(() async {
      ginormousPackageGraph = await _testPackageGraphGinormous;
    });

    test('Verify that SDK libraries are not canonical when missing', () {
      expect(ginormousPackageGraph.publicPackages, isNotEmpty);
    });

    test(
        'Verify that autoIncludeDependencies makes everything document locally',
        () {
      expect(ginormousPackageGraph.packages.map((p) => p.documentedWhere),
          everyElement((DocumentLocation x) => x == DocumentLocation.local));
    });

    test('Verify that ginormousPackageGraph takes in the SDK', () {
      expect(
          ginormousPackageGraph.packages.firstWhere((p) => p.isSdk).libraries,
          hasLength(greaterThan(1)));
      expect(
          ginormousPackageGraph.packages
              .firstWhere((p) => p.isSdk)
              .documentedWhere,
          equals(DocumentLocation.local));
    });
  });

  group('Category', () {
    late final PackageGraph ginormousPackageGraph;

    setUpAll(() async {
      ginormousPackageGraph = await _testPackageGraphGinormous;
    });

    test(
        'Verify auto-included dependencies do not use default package category definitions',
        () {
      var IAmAClassWithCategories = ginormousPackageGraph.localPackages
          .firstWhere((p) => p.name == 'test_package_imported')
          .libraries
          .wherePublic
          .named('categoriesExported')
          .classes
          .wherePublic
          .named('IAmAClassWithCategories');
      expect(IAmAClassWithCategories.hasCategoryNames, isTrue);
      expect(IAmAClassWithCategories.categories, hasLength(1));
      expect(
          IAmAClassWithCategories.categories.first.name, equals('Excellent'));
      expect(IAmAClassWithCategories.displayedCategories, isEmpty);
    });

    test('Verify that multiple categories work correctly', () {
      var fakeLibrary = ginormousPackageGraph.localPackages
          .firstWhere((p) => p.name == 'test_package')
          .libraries
          .wherePublic
          .named('fake');
      var BaseForDocComments =
          fakeLibrary.classes.wherePublic.named('BaseForDocComments');
      var SubForDocComments =
          fakeLibrary.classes.wherePublic.named('SubForDocComments');
      expect(BaseForDocComments.hasCategoryNames, isTrue);
      // Display both, with the correct order and display name.
      expect(BaseForDocComments.displayedCategories, hasLength(2));
      expect(
          BaseForDocComments.displayedCategories.first.name, equals('Superb'));
      expect(
          BaseForDocComments.displayedCategories.last.name, equals('Unreal'));
      // Subclasses do not inherit category information.
      expect(SubForDocComments.hasCategoryNames, isTrue);
      expect(SubForDocComments.categories, hasLength(1));
      expect(SubForDocComments.categories.first.isDocumented, isFalse);
      expect(SubForDocComments.displayedCategories, isEmpty);
    });

    test('No duplicate entries', () {
      final categories = ginormousPackageGraph.localPackages
          .firstWhere((p) => p.name == 'test_package')
          .categories;
      for (final c in categories) {
        expect(c.classes.length, equals(c.classes.toSet().length));
        expect(c.exceptions.length, equals(c.exceptions.toSet().length));
        expect(c.extensions.length, equals(c.extensions.toSet().length));
        expect(
            c.extensionTypes.length, equals(c.extensionTypes.toSet().length));
        expect(c.enums.length, equals(c.enums.toSet().length));
        expect(c.mixins.length, equals(c.mixins.toSet().length));
        expect(c.constants.length, equals(c.constants.toSet().length));
        expect(c.properties.length, equals(c.properties.toSet().length));
        expect(c.functions.length, equals(c.functions.toSet().length));
        expect(c.typedefs.length, equals(c.typedefs.toSet().length));
      }
    });
  });

  group('Package', () {
    late final PackageGraph sdkAsPackageGraph;

    setUpAll(() async {
      sdkAsPackageGraph = await _testPackageGraphSdk;
    });

    // Analyzer's MockSdk's html library doesn't conform to the expectations
    // of this test.
    test('Verify Interceptor is hidden from inheritance in docs', () {
      var htmlLibrary =
          sdkAsPackageGraph.libraries.singleWhere((l) => l.name == 'dart:html');
      var eventTarget =
          htmlLibrary.classes.singleWhere((c) => c.name == 'EventTarget');
      var hashCode = eventTarget.instanceFields.wherePublic
          .singleWhere((f) => f.name == 'hashCode');
      expect(
        eventTarget.superChain,
        contains(isA<ParameterizedElementType>()
            .having((t) => t.name, 'name', 'Interceptor')),
        reason: 'EventTarget appears to no longer subtype Interceptor, which '
            'makes the premise of this test invalid. To keep the test case '
            'valid, we need to use a class that subclasses Interceptor.',
      );
      expect(
        hashCode.href,
        equals('${htmlBasePlaceholder}dart-core/Object/hashCode.html'),
        reason:
            "EventTarget appears to have an explicit override of 'hashCode', "
            'which makes this test case invalid.',
      );
      expect(hashCode.canonicalEnclosingContainer!.isDartCoreObject, isTrue);
      expect(
          eventTarget.publicSuperChainReversed
              .any((et) => et.name == 'Interceptor'),
          isFalse);
    });
  });
}
