// Copyright (c) 2021, 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 'dart:async';
import 'dart:io';
import 'dart:isolate';

import 'package:analyzer/dart/analysis/analysis_context.dart';
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:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:graphs/graphs.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';

void main() {
  late _ImportCheck importCheck;
  setUpAll(() async {
    importCheck = await _ImportCheck.create();
  });
  group('backend', () {
    test('must not import from other subdirectories', () async {
      final entryPoints = [
        _testApiLibrary('backend.dart'),
        ...await _ImportCheck.findEntrypointsUnder(
          _testApiLibrary('src/backend'),
        ),
      ];
      await for (final source in importCheck.transitiveSamePackageSources(
        entryPoints,
      )) {
        for (final import in source.imports) {
          expect(
            import.pathSegments.skip(1).take(2),
            ['src', 'backend'],
            reason: 'Invalid import from ${source.uri} : $import',
          );
        }
      }
    });
  });
}

Uri _testApiLibrary(String path) => Uri.parse('package:test_api/$path');

class _ImportCheck {
  final AnalysisContext _context;

  static Future<Iterable<Uri>> findEntrypointsUnder(Uri uri) async {
    if (!uri.path.endsWith('/')) {
      uri = uri.replace(path: '${uri.path}/');
    }
    final directory = p.fromUri(await Isolate.resolvePackageUri(uri));
    return Glob('./**')
        .listSync(root: directory)
        .whereType<File>()
        .map((f) => uri.resolve(p.url.relative(f.path, from: directory)));
  }

  static Future<_ImportCheck> create() async {
    final context = await _createAnalysisContext();
    return _ImportCheck._(context);
  }

  static Future<AnalysisContext> _createAnalysisContext() async {
    final libUri = Uri.parse('package:graphs/');
    final libPath = await _pathForUri(libUri);
    final packagePath = p.dirname(libPath);

    final contexts =
        AnalysisContextCollection(includedPaths: [packagePath]).contexts;
    if (contexts.length != 1) {
      throw StateError('Expected to find exactly one context, got $contexts');
    }
    return contexts.first;
  }

  static Future<String> _pathForUri(Uri uri) async {
    final fileUri = await Isolate.resolvePackageUri(uri);
    if (fileUri == null || !fileUri.isScheme('file')) {
      throw StateError('Expected to resolve $uri to a file URI, got $fileUri');
    }
    return p.fromUri(fileUri);
  }

  _ImportCheck._(this._context);

  Stream<_Source> transitiveSamePackageSources(Iterable<Uri> entryPoints) {
    assert(entryPoints.every((e) => e.scheme == 'package'));
    final package = entryPoints.first.pathSegments.first;
    assert(entryPoints.skip(1).every((e) => e.pathSegments.first == package));
    return crawlAsync<Uri, _Source>(
      entryPoints,
      (uri) async => _Source(uri, await _findImports(uri, package)),
      (_, source) => source.imports,
    );
  }

  Future<Set<Uri>> _findImports(Uri uri, String restrictToPackage) async {
    var path = await _pathForUri(uri);
    var analysisSession = _context.currentSession;
    var parseResult = analysisSession.getParsedUnit(path) as ParsedUnitResult;
    assert(
      parseResult.content.isNotEmpty,
      'Tried to read an invalid library $uri',
    );
    return parseResult.unit.directives
        .whereType<UriBasedDirective>()
        .map((d) => d.uri.stringValue!)
        .where((uri) => !uri.startsWith('dart:'))
        .map((import) => _resolveImport(import, uri))
        .where((import) => import.pathSegments.first == restrictToPackage)
        .toSet();
  }

  static Uri _resolveImport(String import, Uri from) {
    if (import.startsWith('package:')) return Uri.parse(import);
    assert(from.scheme == 'package');
    final package = from.pathSegments.first;
    final fromPath = p.joinAll(from.pathSegments.skip(1));
    final path = p.normalize(p.join(p.dirname(fromPath), import));
    return Uri.parse('package:${p.join(package, path)}');
  }
}

class _Source {
  final Uri uri;
  final Set<Uri> imports;

  _Source(this.uri, this.imports);
}
