// 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.

import 'dart:io';

import 'package:_fe_analyzer_shared/src/testing/id.dart' show ActualData, Id;
import 'package:_fe_analyzer_shared/src/testing/id_testing.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/src/dart/analysis/testing_data.dart';
import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
import 'package:analyzer/src/util/ast_data_extractor.dart';

import '../util/id_testing_helper.dart';

main(List<String> args) {
  Directory dataDir = Directory.fromUri(
    Platform.script.resolve(
      '../../../_fe_analyzer_shared/test/flow_analysis/reachability/data',
    ),
  );
  return runTests<Set<_ReachabilityAssertion>>(
    dataDir,
    args: args,
    createUriForFileName: createUriForFileName,
    onFailure: onFailure,
    runTest: runTestFor(const _ReachabilityDataComputer(), [
      analyzerDefaultConfig,
    ]),
  );
}

enum _ReachabilityAssertion { doesNotComplete, unreachable }

class _ReachabilityDataComputer
    extends DataComputer<Set<_ReachabilityAssertion>> {
  const _ReachabilityDataComputer();

  @override
  DataInterpreter<Set<_ReachabilityAssertion>> get dataValidator =>
      const _ReachabilityDataInterpreter();

  @override
  void computeUnitData(
    TestingData testingData,
    CompilationUnit unit,
    Map<Id, ActualData<Set<_ReachabilityAssertion>>> actualMap,
  ) {
    var unitUri = unit.declaredFragment!.source.uri;
    var flowResult = testingData.uriToFlowAnalysisData[unitUri]!;
    _ReachabilityDataExtractor(unitUri, actualMap, flowResult).run(unit);
  }
}

class _ReachabilityDataExtractor
    extends AstDataExtractor<Set<_ReachabilityAssertion>> {
  final FlowAnalysisDataForTesting _flowResult;

  _ReachabilityDataExtractor(super.uri, super.actualMap, this._flowResult);

  @override
  Set<_ReachabilityAssertion>? computeNodeValue(Id id, AstNode node) {
    Set<_ReachabilityAssertion> result = {};
    if (node is Expression && node.parent is ExpressionStatement) {
      // The reachability of an expression statement and the statement it
      // contains should always be the same.  We check this with an assert
      // statement, and only annotate the expression statement, to reduce the
      // amount of redundancy in the test files.
      assert(
        _flowResult.unreachableNodes.contains(node) ==
            _flowResult.unreachableNodes.contains(node.parent),
      );
    } else if (_flowResult.unreachableNodes.contains(node)) {
      result.add(_ReachabilityAssertion.unreachable);
    }
    if (node is FunctionDeclaration) {
      _checkBodyCompletion(node.functionExpression.body, result);
    } else if (node is ConstructorDeclaration) {
      _checkBodyCompletion(node.body, result);
    } else if (node is MethodDeclaration) {
      _checkBodyCompletion(node.body, result);
    }
    return result.isEmpty ? null : result;
  }

  void _checkBodyCompletion(
    FunctionBody? body,
    Set<_ReachabilityAssertion> result,
  ) {
    if (body != null &&
        _flowResult.functionBodiesThatDontComplete.contains(body)) {
      result.add(_ReachabilityAssertion.doesNotComplete);
    }
  }
}

class _ReachabilityDataInterpreter
    implements DataInterpreter<Set<_ReachabilityAssertion>> {
  const _ReachabilityDataInterpreter();

  @override
  String getText(
    Set<_ReachabilityAssertion> actualData, [
    String? indentation,
  ]) => _sortedRepresentation(_toStrings(actualData));

  @override
  String? isAsExpected(
    Set<_ReachabilityAssertion> actualData,
    String? expectedData,
  ) {
    var actualStrings = _toStrings(actualData);
    var actualSorted = _sortedRepresentation(actualStrings);
    var expectedSorted = _sortedRepresentation(expectedData?.split(','));
    if (actualSorted == expectedSorted) {
      return null;
    } else {
      return 'Expected $expectedData, got $actualSorted';
    }
  }

  @override
  bool isEmpty(Set<_ReachabilityAssertion> actualData) => actualData.isEmpty;

  String _sortedRepresentation(Iterable<String>? values) {
    var list = values == null || values.isEmpty ? ['none'] : values.toList();
    list.sort();
    return list.join(',');
  }

  List<String> _toStrings(Set<_ReachabilityAssertion> actualData) => actualData
      .map((flowAssertion) => flowAssertion.toString().split('.')[1])
      .toList();
}
