// Copyright (c) 2018, 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/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/src/context/context.dart';
import 'package:analyzer/src/dart/scanner/reader.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/parser.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:analyzer/src/summary/format.dart';
import 'package:analyzer/src/summary/idl.dart';
import 'package:analyzer/src/summary/link.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/summary/prelink.dart';
import 'package:analyzer/src/summary/summarize_ast.dart';
import 'package:analyzer/src/summary/summarize_elements.dart';
import 'package:analyzer/src/task/api/dart.dart';
import 'package:analyzer/src/task/api/general.dart';
import 'package:path/path.dart' show posix;
import 'package:test/test.dart';

import '../context/mock_sdk.dart';
import 'resynthesize_common.dart';

/// Convert the given Posix style file [path] to the corresponding absolute URI.
String absUri(String path) {
  String absolutePath = posix.absolute(path);
  return posix.toUri(absolutePath).toString();
}

CompilationUnit _parseText(String text) {
  CharSequenceReader reader = new CharSequenceReader(text);
  Scanner scanner =
      new Scanner(null, reader, AnalysisErrorListener.NULL_LISTENER);
  Token token = scanner.tokenize();
  Parser parser = new Parser(
      NonExistingSource.unknown, AnalysisErrorListener.NULL_LISTENER);
  CompilationUnit unit = parser.parseCompilationUnit(token);
  unit.lineInfo = new LineInfo(scanner.lineStarts);
  return unit;
}

/// Verify invariants of the given [linkedLibrary].
void _validateLinkedLibrary(LinkedLibrary linkedLibrary) {
  for (LinkedUnit unit in linkedLibrary.units) {
    for (LinkedReference reference in unit.references) {
      switch (reference.kind) {
        case ReferenceKind.classOrEnum:
        case ReferenceKind.topLevelPropertyAccessor:
        case ReferenceKind.topLevelFunction:
        case ReferenceKind.typedef:
          // This reference can have either a zero or a nonzero dependency,
          // since it refers to top level element which might or might not be
          // imported from another library.
          break;
        case ReferenceKind.prefix:
          // Prefixes should have a dependency of 0, since they come from the
          // current library.
          expect(reference.dependency, 0,
              reason: 'Nonzero dependency for prefix');
          break;
        case ReferenceKind.unresolved:
          // Unresolved references always have a dependency of 0.
          expect(reference.dependency, 0,
              reason: 'Nonzero dependency for undefined');
          break;
        default:
          // This reference should have a dependency of 0, since it refers to
          // an element that is contained within some other element.
          expect(reference.dependency, 0,
              reason: 'Nonzero dependency for ${reference.kind}');
      }
    }
  }
}

/// Abstract base class for tests of summary resynthesis.
///
/// Test classes should not extend this class directly; they should extend a
/// class that implements this class with methods that drive summary generation.
/// The tests themselves can then be provided via mixin, allowing summaries to
/// be tested in a variety of ways.
abstract class ResynthesizeTestStrategy {
  //Future<LibraryElementImpl> checkLibrary(String text,
  //    {bool allowErrors: false, bool dumpSummaries: false});

  void set allowMissingFiles(bool value);

  AnalysisContextImpl get context;

  MemoryResourceProvider get resourceProvider;

  void set testFile(String value);

  Source get testSource;

  void addLibrary(String uri);

  Source addLibrarySource(String filePath, String contents);

  Source addSource(String path, String contents);

  Source addTestSource(String code, [Uri uri]);

  void checkMinimalResynthesisWork(
      TestSummaryResynthesizer resynthesizer, LibraryElement library);

  TestSummaryResynthesizer encodeLibrary(Source source);

  void prepareAnalysisContext([AnalysisOptions options]);
}

/// Implementation of [SummaryBlackBoxTestStrategy] that drives summary
/// generation using the old two-phase API.
class ResynthesizeTestStrategyTwoPhase extends AbstractResynthesizeTest
    implements ResynthesizeTestStrategy {
  final Set<Source> serializedSources = new Set<Source>();

  final Map<String, UnlinkedUnitBuilder> uriToUnit =
      <String, UnlinkedUnitBuilder>{};

  PackageBundleAssembler bundleAssembler = new PackageBundleAssembler();

  TestSummaryResynthesizer encodeLibrary(Source source) {
    _serializeLibrary(source);

    PackageBundle bundle =
        new PackageBundle.fromBuffer(bundleAssembler.assemble().toBuffer());

    Map<String, UnlinkedUnit> unlinkedSummaries = <String, UnlinkedUnit>{};
    for (int i = 0; i < bundle.unlinkedUnitUris.length; i++) {
      String uri = bundle.unlinkedUnitUris[i];
      unlinkedSummaries[uri] = bundle.unlinkedUnits[i];
    }

    LinkedLibrary getDependency(String absoluteUri) {
      Map<String, LinkedLibrary> sdkLibraries =
          SerializedMockSdk.instance.uriToLinkedLibrary;
      LinkedLibrary linkedLibrary = sdkLibraries[absoluteUri];
      if (linkedLibrary == null && !allowMissingFiles) {
        fail('Linker unexpectedly requested LinkedLibrary for "$absoluteUri".'
            '  Libraries available: ${sdkLibraries.keys}');
      }
      return linkedLibrary;
    }

    UnlinkedUnit getUnit(String absoluteUri) {
      UnlinkedUnit unit = uriToUnit[absoluteUri] ??
          SerializedMockSdk.instance.uriToUnlinkedUnit[absoluteUri];
      if (unit == null && !allowMissingFiles) {
        fail('Linker unexpectedly requested unit for "$absoluteUri".');
      }
      return unit;
    }

    Set<String> nonSdkLibraryUris = serializedSources
        .where((Source source) =>
            !source.isInSystemLibrary &&
            context.computeKindOf(source) == SourceKind.LIBRARY)
        .map((Source source) => source.uri.toString())
        .toSet();

    Map<String, LinkedLibrary> linkedSummaries = link(nonSdkLibraryUris,
        getDependency, getUnit, context.declaredVariables.get);

    return new TestSummaryResynthesizer(
        context,
        new Map<String, UnlinkedUnit>()
          ..addAll(SerializedMockSdk.instance.uriToUnlinkedUnit)
          ..addAll(unlinkedSummaries),
        new Map<String, LinkedLibrary>()
          ..addAll(SerializedMockSdk.instance.uriToLinkedLibrary)
          ..addAll(linkedSummaries),
        allowMissingFiles);
  }

  UnlinkedUnit _getUnlinkedUnit(Source source) {
    if (source == null) {
      return new UnlinkedUnitBuilder();
    }

    String uriStr = source.uri.toString();
    {
      UnlinkedUnit unlinkedUnitInSdk =
          SerializedMockSdk.instance.uriToUnlinkedUnit[uriStr];
      if (unlinkedUnitInSdk != null) {
        return unlinkedUnitInSdk;
      }
    }
    return uriToUnit.putIfAbsent(uriStr, () {
      int modificationTime = context.computeResult(source, MODIFICATION_TIME);
      if (modificationTime < 0) {
        // Source does not exist.
        if (!allowMissingFiles) {
          fail('Unexpectedly tried to get unlinked summary for $source');
        }
        return null;
      }
      CompilationUnit unit = context.computeResult(source, PARSED_UNIT);
      UnlinkedUnitBuilder unlinkedUnit = serializeAstUnlinked(unit);
      bundleAssembler.addUnlinkedUnit(source, unlinkedUnit);
      return unlinkedUnit;
    });
  }

  void _serializeLibrary(Source librarySource) {
    if (librarySource == null || librarySource.isInSystemLibrary) {
      return;
    }
    if (!serializedSources.add(librarySource)) {
      return;
    }

    UnlinkedUnit getPart(String absoluteUri) {
      Source source = context.sourceFactory.forUri(absoluteUri);
      return _getUnlinkedUnit(source);
    }

    UnlinkedPublicNamespace getImport(String relativeUri) {
      return getPart(relativeUri)?.publicNamespace;
    }

    UnlinkedUnit definingUnit = _getUnlinkedUnit(librarySource);
    if (definingUnit != null) {
      LinkedLibraryBuilder linkedLibrary = prelink(librarySource.uri.toString(),
          definingUnit, getPart, getImport, context.declaredVariables.get);
      linkedLibrary.dependencies.skip(1).forEach((LinkedDependency d) {
        Source source = context.sourceFactory.forUri(d.uri);
        _serializeLibrary(source);
      });
    }
  }
}

/// [SerializedMockSdk] is a singleton class representing the result of
/// serializing the mock SDK to summaries.  It is computed once and then shared
/// among test invocations so that we don't bog down the tests.
///
/// Note: should an exception occur during computation of [instance], it will
/// silently be set to null to allow other tests to complete quickly.
class SerializedMockSdk {
  static final SerializedMockSdk instance = _serializeMockSdk();

  final Map<String, UnlinkedUnit> uriToUnlinkedUnit;

  final Map<String, LinkedLibrary> uriToLinkedLibrary;
  SerializedMockSdk._(this.uriToUnlinkedUnit, this.uriToLinkedLibrary);

  static SerializedMockSdk _serializeMockSdk() {
    try {
      Map<String, UnlinkedUnit> uriToUnlinkedUnit = <String, UnlinkedUnit>{};
      Map<String, LinkedLibrary> uriToLinkedLibrary = <String, LinkedLibrary>{};
      PackageBundle bundle = new MockSdk().getLinkedBundle();
      for (int i = 0; i < bundle.unlinkedUnitUris.length; i++) {
        String uri = bundle.unlinkedUnitUris[i];
        uriToUnlinkedUnit[uri] = bundle.unlinkedUnits[i];
      }
      for (int i = 0; i < bundle.linkedLibraryUris.length; i++) {
        String uri = bundle.linkedLibraryUris[i];
        uriToLinkedLibrary[uri] = bundle.linkedLibraries[i];
      }
      return new SerializedMockSdk._(uriToUnlinkedUnit, uriToLinkedLibrary);
    } catch (_) {
      return null;
    }
  }
}

/// Abstract base class for tests involving summaries.
///
/// Test classes should not extend this class directly; they should extend a
/// class that implements this class with methods that drive summary generation.
/// The tests themselves can then be provided via mixin, allowing summaries to
/// be tested in a variety of ways.
abstract class SummaryBaseTestStrategy {
  /// Add the given package bundle as a dependency so that it may be referenced
  /// by the files under test.
  void addBundle(String path, PackageBundle bundle);

  /// Add the given source file so that it may be referenced by the file under
  /// test.
  void addNamedSource(String filePath, String contents);

  /// Link together the given file, along with any other files passed to
  /// [addNamedSource], to form a package bundle.  Reset the state of the
  /// buffers accumulated by [addNamedSource] and [addBundle] so that further
  /// bundles can be created.
  PackageBundleBuilder createPackageBundle(String text,
      {String path: '/test.dart', String uri});
}

/// Abstract base class for black-box tests of summaries.
///
/// Test classes should not extend this class directly; they should extend a
/// class that implements this class with methods that drive summary generation.
/// The tests themselves can then be provided via mixin, allowing summaries to
/// be tested in a variety of ways.
abstract class SummaryBlackBoxTestStrategy extends SummaryBaseTestStrategy {
  /// A test will set this to `true` if it contains `import`, `export`, or
  /// `part` declarations that deliberately refer to non-existent files.
  void set allowMissingFiles(bool value);

  /// Indicates whether the summary contains expressions for non-const fields.
  ///
  /// When one-phase summarization is in use, only const field initializer
  /// expressions are stored in the summary.
  bool get containsNonConstExprs;

  /// Get access to the linked summary that results from serializing and
  /// then deserializing the library under test.
  LinkedLibrary get linked;

  /// `true` if the linked portion of the summary only contains prelinked data.
  /// This happens because we don't yet have a full linker; only a prelinker.
  bool get skipFullyLinkedData;

  /// Get access to the unlinked compilation unit summaries that result from
  /// serializing and deserializing the library under test.
  List<UnlinkedUnit> get unlinkedUnits;

  /// Serialize the given library [text], then deserialize it and store its
  /// summary in [lib].
  void serializeLibraryText(String text, {bool allowErrors: false});
}

/// Implementation of [SummaryBlackBoxTestStrategy] that drives summary
/// generation using the old two-phase API, and exercises the pre-linker only.
class SummaryBlackBoxTestStrategyPrelink
    extends _SummaryBlackBoxTestStrategyTwoPhase
    implements SummaryBlackBoxTestStrategy {
  @override
  bool get skipFullyLinkedData => true;

  @override
  void serializeLibraryText(String text, {bool allowErrors: false}) {
    super.serializeLibraryText(text, allowErrors: allowErrors);

    UnlinkedUnit getPart(String absoluteUri) {
      return _linkerInputs.getUnit(absoluteUri);
    }

    UnlinkedPublicNamespace getImport(String absoluteUri) {
      return getPart(absoluteUri)?.publicNamespace;
    }

    linked = new LinkedLibrary.fromBuffer(prelink(
        _linkerInputs._testDartUri.toString(),
        _linkerInputs._unlinkedDefiningUnit,
        getPart,
        getImport,
        (String declaredVariable) => null).toBuffer());
    _validateLinkedLibrary(linked);
  }
}

/// Implementation of [SummaryBlackBoxTestStrategy] that drives summary
/// generation using the old two-phase API, and exercises full summary
/// generation.
class SummaryBlackBoxTestStrategyTwoPhase
    extends _SummaryBlackBoxTestStrategyTwoPhase
    implements SummaryBlackBoxTestStrategy {
  @override
  bool get skipFullyLinkedData => false;
}

/// Abstract base class for unit tests of the summary linker.
///
/// Test classes should not extend this class directly; they should extend a
/// class that implements this class with methods that drive summary generation.
/// The tests themselves can then be provided via mixin, allowing summaries to
/// be tested in a variety of ways.
abstract class SummaryLinkerTestStrategy extends SummaryBaseTestStrategy {
  Linker get linker;

  /// Gets the URI of the main library under test.
  ///
  /// May only be called after [createLinker].
  Uri get testDartUri;

  LibraryElementInBuildUnit get testLibrary;

  void createLinker(String text, {String path: '/test.dart'});
}

/// Implementation of [SummaryLinkerTestStrategy] that drives summary generation
/// using the old two-phase API.
class SummaryLinkerTestStrategyTwoPhase extends _SummaryBaseTestStrategyTwoPhase
    implements SummaryLinkerTestStrategy {
  LibraryElementInBuildUnit _testLibrary;

  @override
  Linker linker;

  @override
  Uri get testDartUri => _linkerInputs._testDartUri;

  @override
  LibraryElementInBuildUnit get testLibrary =>
      _testLibrary ??= linker.getLibrary(_linkerInputs._testDartUri)
          as LibraryElementInBuildUnit;

  @override
  bool get _allowMissingFiles => false;

  @override
  void createLinker(String text, {String path: '/test.dart'}) {
    _linkerInputs = _createLinkerInputs(text, path: path);
    Map<String, LinkedLibraryBuilder> linkedLibraries = setupForLink(
        _linkerInputs.linkedLibraries,
        _linkerInputs.getUnit,
        _linkerInputs.getDeclaredVariable);
    linker = new Linker(linkedLibraries, _linkerInputs.getDependency,
        _linkerInputs.getUnit, null);
  }
}

/// [_FilesToLink] stores information about a set of files to be linked
/// together.  This information is grouped into a class to allow it to be reset
/// easily when [_SummaryBaseTestStrategyTwoPhase._createLinkerInputs] is
/// called.
///
/// The generic parameter [U] is the type of information stored for each
/// compilation unit.
class _FilesToLink<U> {
  /// Map from absolute URI to the [U] for each compilation unit passed to
  /// [addNamedSource].
  Map<String, U> uriToUnit = <String, U>{};

  /// Information about summaries to be included in the link process.
  SummaryDataStore summaryDataStore = new SummaryDataStore([]);
}

/// Instances of the class [_LinkerInputs] encapsulate the necessary information
/// to pass to the summary linker.
class _LinkerInputs {
  final bool _allowMissingFiles;
  final Map<String, UnlinkedUnit> _uriToUnit;
  final Uri _testDartUri;
  final UnlinkedUnit _unlinkedDefiningUnit;
  final Map<String, LinkedLibrary> _dependentLinkedLibraries;
  final Map<String, UnlinkedUnit> _dependentUnlinkedUnits;

  _LinkerInputs(
      this._allowMissingFiles,
      this._uriToUnit,
      this._testDartUri,
      this._unlinkedDefiningUnit,
      this._dependentLinkedLibraries,
      this._dependentUnlinkedUnits);

  Set<String> get linkedLibraries => _uriToUnit.keys.toSet();

  String getDeclaredVariable(String name) {
    return null;
  }

  LinkedLibrary getDependency(String absoluteUri) {
    Map<String, LinkedLibrary> sdkLibraries =
        SerializedMockSdk.instance.uriToLinkedLibrary;
    LinkedLibrary linkedLibrary =
        sdkLibraries[absoluteUri] ?? _dependentLinkedLibraries[absoluteUri];
    if (linkedLibrary == null && !_allowMissingFiles) {
      Set<String> librariesAvailable = sdkLibraries.keys.toSet();
      librariesAvailable.addAll(_dependentLinkedLibraries.keys);
      fail('Linker unexpectedly requested LinkedLibrary for "$absoluteUri".'
          '  Libraries available: ${librariesAvailable.toList()}');
    }
    return linkedLibrary;
  }

  UnlinkedUnit getUnit(String absoluteUri) {
    if (absoluteUri == null) {
      return null;
    }
    UnlinkedUnit unit = _uriToUnit[absoluteUri] ??
        SerializedMockSdk.instance.uriToUnlinkedUnit[absoluteUri] ??
        _dependentUnlinkedUnits[absoluteUri];
    if (unit == null && !_allowMissingFiles) {
      fail('Linker unexpectedly requested unit for "$absoluteUri".');
    }
    return unit;
  }
}

/// Implementation of [SummaryBaseTestStrategy] that drives summary generation
/// using the old two-phase API.
abstract class _SummaryBaseTestStrategyTwoPhase
    implements SummaryBaseTestStrategy {
  /// Information about the files to be linked.
  _FilesToLink<UnlinkedUnitBuilder> _filesToLink =
      new _FilesToLink<UnlinkedUnitBuilder>();

  _LinkerInputs _linkerInputs;

  bool get _allowMissingFiles;

  @override
  void addBundle(String path, PackageBundle bundle) {
    _filesToLink.summaryDataStore.addBundle(path, bundle);
  }

  @override
  void addNamedSource(String filePath, String contents) {
    CompilationUnit unit = _parseText(contents);
    UnlinkedUnitBuilder unlinkedUnit = serializeAstUnlinked(unit);
    _filesToLink.uriToUnit[absUri(filePath)] = unlinkedUnit;
  }

  @override
  PackageBundleBuilder createPackageBundle(String text,
      {String path: '/test.dart', String uri}) {
    PackageBundleAssembler assembler = new PackageBundleAssembler();
    _LinkerInputs linkerInputs =
        _createLinkerInputs(text, path: path, uri: uri);
    Map<String, LinkedLibraryBuilder> linkedLibraries = link(
        linkerInputs.linkedLibraries,
        linkerInputs.getDependency,
        linkerInputs.getUnit,
        linkerInputs.getDeclaredVariable);
    linkedLibraries.forEach(assembler.addLinkedLibrary);
    linkerInputs._uriToUnit.forEach((String uri, UnlinkedUnit unit) {
      assembler.addUnlinkedUnitViaUri(uri, unit);
    });
    return assembler.assemble();
  }

  UnlinkedUnitBuilder createUnlinkedSummary(Uri uri, String text) =>
      serializeAstUnlinked(_parseText(text));

  _LinkerInputs _createLinkerInputs(String text,
      {String path: '/test.dart', String uri}) {
    uri ??= absUri(path);
    Uri testDartUri = Uri.parse(uri);
    UnlinkedUnitBuilder unlinkedDefiningUnit =
        createUnlinkedSummary(testDartUri, text);
    _filesToLink.uriToUnit[testDartUri.toString()] = unlinkedDefiningUnit;
    _LinkerInputs linkerInputs = new _LinkerInputs(
        _allowMissingFiles,
        _filesToLink.uriToUnit,
        testDartUri,
        unlinkedDefiningUnit,
        _filesToLink.summaryDataStore.linkedMap,
        _filesToLink.summaryDataStore.unlinkedMap);
    // Reset _filesToLink in case the test needs to start a new package bundle.
    _filesToLink = new _FilesToLink<UnlinkedUnitBuilder>();
    return linkerInputs;
  }
}

/// Implementation of [SummaryBlackBoxTestStrategy] that drives summary
/// generation using the old two-phase API.
///
/// Not intended to be used directly; instead use a derived class that either
/// exercises the full summary algorithmm or just pre-linking.
abstract class _SummaryBlackBoxTestStrategyTwoPhase
    extends _SummaryBaseTestStrategyTwoPhase
    implements SummaryBlackBoxTestStrategy {
  @override
  List<UnlinkedUnit> unlinkedUnits;

  @override
  LinkedLibrary linked;

  @override
  bool _allowMissingFiles = false;

  @override
  void set allowMissingFiles(bool value) {
    _allowMissingFiles = value;
  }

  @override
  bool get containsNonConstExprs => true;

  @override
  void serializeLibraryText(String text, {bool allowErrors: false}) {
    Map<String, UnlinkedUnitBuilder> uriToUnit = this._filesToLink.uriToUnit;
    _linkerInputs = _createLinkerInputs(text);
    linked = link(
        _linkerInputs.linkedLibraries,
        _linkerInputs.getDependency,
        _linkerInputs.getUnit,
        (name) => null)[_linkerInputs._testDartUri.toString()];
    expect(linked, isNotNull);
    _validateLinkedLibrary(linked);
    unlinkedUnits = <UnlinkedUnit>[_linkerInputs._unlinkedDefiningUnit];
    for (String relativeUriStr
        in _linkerInputs._unlinkedDefiningUnit.publicNamespace.parts) {
      Uri relativeUri;
      try {
        relativeUri = Uri.parse(relativeUriStr);
      } on FormatException {
        unlinkedUnits.add(new UnlinkedUnitBuilder());
        continue;
      }

      UnlinkedUnit unit = uriToUnit[
          resolveRelativeUri(_linkerInputs._testDartUri, relativeUri)
              .toString()];
      if (unit == null) {
        if (!_allowMissingFiles) {
          fail('Test referred to unknown unit $relativeUriStr');
        }
      } else {
        unlinkedUnits.add(unit);
      }
    }
  }
}
