// Copyright (c) 2023, 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/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer_testing/package_root.dart' as package_root;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(SelectionCoverageTest);
  });
}

class AstImplData {
  final List<ClassElement> instantiableInterfaces = [];

  new();
}

class AstInterfaceData {
  /// A table mapping the element for a class to a list of the elements for the
  /// supertypes of that class.
  final Map<ClassElement, List<ClassElement>> supertypes = {};

  /// A table mapping class element to a list of the getters declared directly
  /// in that class that return a `NodeList`.
  final Map<ClassElement, List<ExecutableElement>> declaredLists = {};

  new();

  List<ExecutableElement> nodeListsFor(ClassElement class_) {
    var lists = <ExecutableElement>[];
    _addListsFor(lists, class_, <ClassElement>{});
    return lists;
  }

  void _addListsFor(
    List<ExecutableElement> lists,
    ClassElement class_,
    Set<ClassElement> visited,
  ) {
    if (!visited.add(class_)) {
      return;
    }
    lists.addAll(declaredLists[class_] ?? const []);
    var supertypes = this.supertypes[class_];
    if (supertypes != null) {
      for (var supertype in supertypes) {
        _addListsFor(lists, supertype, visited);
      }
    }
  }
}

@reflectiveTest
class SelectionCoverageTest {
  AstImplData processAstImpl(ResolvedUnitResult result) {
    var data = AstImplData();

    for (var declaration in result.unit.declarations) {
      if (declaration is ClassDeclaration &&
          declaration.abstractKeyword == null) {
        var interfaceName = declaration.namePart.typeName.lexeme;
        if (!interfaceName.endsWith('Impl')) {
          continue;
        }
        interfaceName = interfaceName.substring(0, interfaceName.length - 4);
        var implementsClause = declaration.implementsClause;
        if (implementsClause != null) {
          for (var type in implementsClause.interfaces) {
            var element = type.type?.element;
            if (element is ClassElement && element.name == interfaceName) {
              data.instantiableInterfaces.add(element);
            }
          }
        }
      }
    }

    return data;
  }

  AstInterfaceData processAstInterface(ResolvedUnitResult result) {
    var data = AstInterfaceData();

    for (var declaration in result.unit.declarations) {
      if (declaration is ClassDeclaration) {
        // Build the subtype map.
        var subtypeElement = declaration.declaredFragment!.element;
        var implementsClause = declaration.implementsClause;
        if (implementsClause != null) {
          for (var supertype in implementsClause.interfaces) {
            var supertypeElement = supertype.type?.element;
            if (supertypeElement is ClassElement) {
              data.supertypes
                  .putIfAbsent(subtypeElement, () => [])
                  .add(supertypeElement);
            }
          }
        }

        // Build the node list map.
        var nodeLists = <ExecutableElement>[];
        for (var member in declaration.body.members) {
          if (member is MethodDeclaration && member.isGetter) {
            var returnType = member.returnType;
            if (returnType != null &&
                returnType.toSource().startsWith('NodeList<')) {
              nodeLists.add(member.declaredFragment!.element);
            }
          }
        }
        if (nodeLists.isNotEmpty) {
          data.declaredLists[subtypeElement] = nodeLists;
        }
      }
    }

    return data;
  }

  SelectionData processSelection(ResolvedUnitResult result) {
    var data = SelectionData();

    for (var declaration in result.unit.declarations) {
      if (declaration is ClassDeclaration &&
          declaration.namePart.typeName.lexeme == '_ChildrenFinder') {
        for (var member in declaration.body.members) {
          if (member is MethodDeclaration &&
              member.name.lexeme.startsWith('visit')) {
            var visitedClass = member
                .parameters
                ?.parameters
                .first
                .declaredFragment
                ?.element
                .type
                .element;
            if (visitedClass is ClassElement) {
              var visitor = VisitMethodVisitor();
              member.body.accept(visitor);
              data.visitedLists[visitedClass] = visitor.visitedLists;
            }
          }
        }
      }
    }

    return data;
  }

  @TestTimeout(Timeout.factor(4))
  Future<void> test_visitorCoverage() async {
    var provider = PhysicalResourceProvider.INSTANCE;
    var pathContext = provider.pathContext;
    var packageRoot = pathContext.normalize(package_root.packageRoot);
    var pathToAsInterface = pathContext.join(
      packageRoot,
      'analyzer',
      'lib',
      'dart',
      'ast',
      'ast.dart',
    );
    var pathToAsImpl = pathContext.join(
      packageRoot,
      'analyzer',
      'lib',
      'src',
      'dart',
      'ast',
      'ast.dart',
    );
    var pathToSelection = pathContext.join(
      packageRoot,
      'analysis_server',
      'lib',
      'src',
      'utilities',
      'selection.dart',
    );

    var collection = AnalysisContextCollection(
      includedPaths: [pathToSelection],
    );
    var context = collection.contexts.first;
    var astInterfaceResult = await context.currentSession.getResolvedUnit(
      pathToAsInterface,
    );
    var astImplResult = await context.currentSession.getResolvedUnit(
      pathToAsImpl,
    );
    var selectionResult = await context.currentSession.getResolvedUnit(
      pathToSelection,
    );

    var astInterfaceData = processAstInterface(
      astInterfaceResult as ResolvedUnitResult,
    );
    var astImplData = processAstImpl(astImplResult as ResolvedUnitResult);
    var selectionData = processSelection(selectionResult as ResolvedUnitResult);
    var visitedLists = selectionData.visitedLists;

    var buffer = StringBuffer();
    for (var interface in astImplData.instantiableInterfaces) {
      if (interface.name == 'Comment' ||
          interface.name == 'VariableDeclaration') {
        // The class `Comment` has references, but we don't support selecting a
        // portion of a comment in order to operate on it.
        //
        // The class `VariableDeclaration` has metadata, but the list is never
        // populated, so we don't visit the class, and hence are required to
        // special case it here. If the class hierarchy is ever cleaned up, we
        // can remove this special casing.
        continue;
      }
      var declaredNodeLists = astInterfaceData.nodeListsFor(interface);
      if (declaredNodeLists.isEmpty) {
        continue;
      }
      var visitedNodeLists = visitedLists[interface];
      if (visitedNodeLists == null) {
        var interfaceName = interface.name;
        buffer.writeln('Missing implementation of visit$interfaceName:');
        buffer.writeln();
        buffer.writeln('@override');
        buffer.writeln('void visit$interfaceName($interfaceName node) {');
        for (var nodeList in declaredNodeLists) {
          buffer.writeln('  _fromList(node.${nodeList.name});');
        }
        buffer.writeln('}');
        buffer.writeln();
      } else {
        var unvisitedNodeLists = {...declaredNodeLists};
        for (var visitedNodeList in visitedNodeLists) {
          unvisitedNodeLists.remove(visitedNodeList);
          var enclosingElement =
              visitedNodeList.enclosingElement as InterfaceElement;
          var overridden = enclosingElement.getOverridden(
            Name(visitedNodeList.library.uri, visitedNodeList.name!),
          );
          if (overridden != null) {
            unvisitedNodeLists.removeAll(overridden);
          }
        }
        if (unvisitedNodeLists.isNotEmpty) {
          buffer.writeln('Missing lines in visit${interface.name}:');
          buffer.writeln();
          for (var nodeList in unvisitedNodeLists) {
            buffer.writeln('  _fromList(node.${nodeList.name});');
          }
          buffer.writeln();
        }
      }
    }
    if (buffer.length > 0) {
      fail(buffer.toString());
    }
  }
}

class SelectionData {
  final Map<ClassElement, List<ExecutableElement>> visitedLists = {};

  new();
}

class VisitMethodVisitor extends RecursiveAstVisitor<void> {
  List<ExecutableElement> visitedLists = [];

  new();

  @override
  void visitMethodInvocation(MethodInvocation node) {
    if (node.methodName.name == '_fromList') {
      var argument = node.argumentList.arguments.first;
      if (argument is PrefixedIdentifier) {
        visitedLists.add(argument.identifier.element as ExecutableElement);
      } else if (argument is PropertyAccess) {
        visitedLists.add(argument.propertyName.element as ExecutableElement);
      }
    }
  }
}
