// 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 'package:_fe_analyzer_shared/src/type_inference/assigned_variables.dart';
import 'package:test/test.dart';

main() {
  test('readCapturedAnywhere records reads in closures', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var v3 = _Variable('v3');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.declare(v3);
    assignedVariables.read(v1);
    assignedVariables.beginNode();
    assignedVariables.read(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.read(v3);
    assignedVariables.finish();
    expect(assignedVariables.readCapturedAnywhere,
        {assignedVariables.keyForVariable(v2)});
  });

  test('readCapturedAnywhere does not record variables local to a closure', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.declare(v2);
    assignedVariables.read(v1);
    assignedVariables.read(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.finish();
    expect(assignedVariables.readCapturedAnywhere,
        {assignedVariables.keyForVariable(v1)});
  });

  test('capturedAnywhere records assignments in closures', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var v3 = _Variable('v3');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.declare(v3);
    assignedVariables.write(v1);
    assignedVariables.beginNode();
    assignedVariables.write(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.write(v3);
    assignedVariables.finish();
    expect(assignedVariables.capturedAnywhere,
        {assignedVariables.keyForVariable(v2)});
  });

  test('capturedAnywhere does not record variables local to a closure', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.declare(v2);
    assignedVariables.write(v1);
    assignedVariables.write(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.finish();
    expect(assignedVariables.capturedAnywhere,
        {assignedVariables.keyForVariable(v1)});
  });

  test('readAnywhere records all reads', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var v3 = _Variable('v3');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.declare(v3);
    assignedVariables.read(v1);
    assignedVariables.beginNode();
    assignedVariables.read(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.read(v3);
    assignedVariables.finish();
    expect(assignedVariables.readAnywhere, {
      assignedVariables.keyForVariable(v1),
      assignedVariables.keyForVariable(v2),
      assignedVariables.keyForVariable(v3)
    });
  });

  test('writtenAnywhere records all assignments', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var v3 = _Variable('v3');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.declare(v3);
    assignedVariables.write(v1);
    assignedVariables.beginNode();
    assignedVariables.write(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.write(v3);
    assignedVariables.finish();
    expect(assignedVariables.writtenAnywhere, {
      assignedVariables.keyForVariable(v1),
      assignedVariables.keyForVariable(v2),
      assignedVariables.keyForVariable(v3)
    });
  });

  test('readInNode ignores reads outside the node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.read(v1);
    assignedVariables.beginNode();
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.read(v2);
    assignedVariables.finish();
    expect(assignedVariables.readInNode(node), isEmpty);
  });

  test('readInNode records reads inside the node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.read(v1);
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(assignedVariables.readInNode(node),
        {assignedVariables.keyForVariable(v1)});
  });

  test('readInNode records reads in a nested node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.read(v1);
    assignedVariables.endNode(_Node());
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(assignedVariables.readInNode(node),
        {assignedVariables.keyForVariable(v1)});
  });

  test('readInNode records reads in a closure', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.read(v1);
    var node = _Node();
    assignedVariables.endNode(node, isClosureOrLateVariableInitializer: true);
    assignedVariables.finish();
    expect(assignedVariables.readInNode(node),
        {assignedVariables.keyForVariable(v1)});
  });

  test('writtenInNode ignores assignments outside the node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.write(v1);
    assignedVariables.beginNode();
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.write(v2);
    assignedVariables.finish();
    expect(assignedVariables.writtenInNode(node), isEmpty);
  });

  test('writtenInNode records assignments inside the node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(assignedVariables.writtenInNode(node),
        {assignedVariables.keyForVariable(v1)});
  });

  test('writtenInNode records assignments in a nested node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    assignedVariables.endNode(_Node());
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(assignedVariables.writtenInNode(node),
        {assignedVariables.keyForVariable(v1)});
  });

  test('writtenInNode records assignments in a closure', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    var node = _Node();
    assignedVariables.endNode(node, isClosureOrLateVariableInitializer: true);
    assignedVariables.finish();
    expect(assignedVariables.writtenInNode(node),
        {assignedVariables.keyForVariable(v1)});
  });

  test('readCapturedInNode ignores reads in non-nested closures', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.beginNode();
    assignedVariables.read(v1);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.beginNode();
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.beginNode();
    assignedVariables.read(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.finish();
    expect(assignedVariables.readCapturedInNode(node), isEmpty);
  });

  test('readCapturedInNode records assignments in nested closures', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.read(v1);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    var innerNode = _Node();
    assignedVariables.endNode(innerNode);
    var outerNode = _Node();
    assignedVariables.endNode(outerNode);
    assignedVariables.finish();
    expect(assignedVariables.readCapturedInNode(innerNode),
        {assignedVariables.keyForVariable(v1)});
    expect(assignedVariables.readCapturedInNode(outerNode),
        {assignedVariables.keyForVariable(v1)});
  });

  test('capturedInNode ignores assignments in non-nested closures', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.beginNode();
    var node = _Node();
    assignedVariables.endNode(node);
    assignedVariables.beginNode();
    assignedVariables.write(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.finish();
    expect(assignedVariables.capturedInNode(node), isEmpty);
  });

  test('capturedInNode records assignments in nested closures', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    assignedVariables.declare(v1);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    var innerNode = _Node();
    assignedVariables.endNode(innerNode);
    var outerNode = _Node();
    assignedVariables.endNode(outerNode);
    assignedVariables.finish();
    expect(assignedVariables.capturedInNode(innerNode),
        {assignedVariables.keyForVariable(v1)});
    expect(assignedVariables.capturedInNode(outerNode),
        {assignedVariables.keyForVariable(v1)});
  });

  group('Variables do not percolate beyond the scope they were declared in',
      () {
    test('Non-closure scope', () {
      var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
      var v1 = _Variable('v1');
      var v2 = _Variable('v2');
      assignedVariables.beginNode();
      assignedVariables.beginNode();
      assignedVariables.declare(v1);
      assignedVariables.declare(v2);
      assignedVariables.read(v1);
      assignedVariables.write(v1);
      assignedVariables.beginNode();
      assignedVariables.read(v2);
      assignedVariables.write(v2);
      assignedVariables.endNode(_Node(),
          isClosureOrLateVariableInitializer: true);
      var innerNode = _Node();
      assignedVariables.endNode(innerNode,
          isClosureOrLateVariableInitializer: false);
      var outerNode = _Node();
      assignedVariables.endNode(outerNode);
      assignedVariables.finish();
      expect(assignedVariables.readInNode(innerNode), isEmpty);
      expect(assignedVariables.writtenInNode(innerNode), isEmpty);
      expect(assignedVariables.readCapturedInNode(innerNode), isEmpty);
      expect(assignedVariables.capturedInNode(innerNode), isEmpty);
      expect(assignedVariables.readInNode(outerNode), isEmpty);
      expect(assignedVariables.writtenInNode(outerNode), isEmpty);
      expect(assignedVariables.readCapturedInNode(outerNode), isEmpty);
      expect(assignedVariables.capturedInNode(outerNode), isEmpty);
    });

    test('Closure scope', () {
      var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
      var v1 = _Variable('v1');
      var v2 = _Variable('v2');
      assignedVariables.beginNode();
      assignedVariables.beginNode();
      assignedVariables.declare(v1);
      assignedVariables.declare(v2);
      assignedVariables.read(v1);
      assignedVariables.write(v1);
      assignedVariables.beginNode();
      assignedVariables.read(v2);
      assignedVariables.write(v2);
      assignedVariables.endNode(_Node(),
          isClosureOrLateVariableInitializer: true);
      var innerNode = _Node();
      assignedVariables.endNode(innerNode,
          isClosureOrLateVariableInitializer: true);
      var outerNode = _Node();
      assignedVariables.endNode(outerNode);
      assignedVariables.finish();
      expect(assignedVariables.readInNode(innerNode), isEmpty);
      expect(assignedVariables.writtenInNode(innerNode), isEmpty);
      expect(assignedVariables.readCapturedInNode(innerNode), isEmpty);
      expect(assignedVariables.capturedInNode(innerNode), isEmpty);
      expect(assignedVariables.readInNode(outerNode), isEmpty);
      expect(assignedVariables.writtenInNode(outerNode), isEmpty);
      expect(assignedVariables.readCapturedInNode(outerNode), isEmpty);
      expect(assignedVariables.capturedInNode(outerNode), isEmpty);
    });
  });

  test('discardNode percolates declarations to enclosing node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var node = _Node();
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.discardNode();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(
        assignedVariables.declaredInNode(node),
        unorderedEquals([
          assignedVariables.keyForVariable(v1),
          assignedVariables.keyForVariable(v2)
        ]));
  });

  test('discardNode percolates reads to enclosing node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var node = _Node();
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.read(v1);
    assignedVariables.read(v2);
    assignedVariables.discardNode();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(
        assignedVariables.readInNode(node),
        unorderedEquals([
          assignedVariables.keyForVariable(v1),
          assignedVariables.keyForVariable(v2)
        ]));
  });

  test('discardNode percolates writes to enclosing node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var node = _Node();
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    assignedVariables.write(v2);
    assignedVariables.discardNode();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(
        assignedVariables.writtenInNode(node),
        unorderedEquals([
          assignedVariables.keyForVariable(v1),
          assignedVariables.keyForVariable(v2)
        ]));
  });

  test('discardNode percolates read captures to enclosing node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var node = _Node();
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.read(v1);
    assignedVariables.read(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.discardNode();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(
        assignedVariables.readCapturedInNode(node),
        unorderedEquals([
          assignedVariables.keyForVariable(v1),
          assignedVariables.keyForVariable(v2)
        ]));
  });

  test('discardNode percolates write captures to enclosing node', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var node = _Node();
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    assignedVariables.write(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    assignedVariables.discardNode();
    assignedVariables.endNode(node);
    assignedVariables.finish();
    expect(
        assignedVariables.capturedInNode(node),
        unorderedEquals([
          assignedVariables.keyForVariable(v1),
          assignedVariables.keyForVariable(v2)
        ]));
  });

  test('deferNode allows deferring of node info', () {
    var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
    var v1 = _Variable('v1');
    var v2 = _Variable('v2');
    var v3 = _Variable('v3');
    var v4 = _Variable('v4');
    assignedVariables.declare(v1);
    assignedVariables.declare(v2);
    assignedVariables.declare(v4);
    assignedVariables.beginNode();
    assignedVariables.write(v1);
    assignedVariables.declare(v3);
    assignedVariables.beginNode();
    assignedVariables.write(v2);
    assignedVariables.endNode(_Node(),
        isClosureOrLateVariableInitializer: true);
    var info = assignedVariables.deferNode();
    assignedVariables.beginNode();
    assignedVariables.write(v4);
    assignedVariables.endNode(_Node());
    var node = _Node();
    assignedVariables.storeInfo(node, info);
    assignedVariables.finish();
    expect(assignedVariables.declaredInNode(node),
        [assignedVariables.keyForVariable(v3)]);
    expect(
        assignedVariables.writtenInNode(node),
        unorderedEquals([
          assignedVariables.keyForVariable(v1),
          assignedVariables.keyForVariable(v2)
        ]));
    expect(assignedVariables.capturedInNode(node),
        [assignedVariables.keyForVariable(v2)]);
  });

  group('Final assertions:', () {
    bool assertionsEnabled = false;
    assert(assertionsEnabled = true);
    var asserts = assertionsEnabled
        ? throwsA(TypeMatcher<AssertionError>())
        : returnsNormally;

    test('finish may not be called twice', () {
      var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
      assignedVariables.finish();
      expect(assignedVariables.finish, asserts);
    });

    test('all deferred infos must be stored', () {
      var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
      assignedVariables.beginNode();
      assignedVariables.deferNode();
      expect(assignedVariables.finish, asserts);
    });

    test('all open nodes must be closed or deferred', () {
      var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
      assignedVariables.beginNode();
      expect(assignedVariables.finish, asserts);
    });

    test('all read variables must be declared', () {
      var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
      var v1 = _Variable('v1');
      assignedVariables.read(v1);
      expect(assignedVariables.finish, asserts);
    });

    test('all written variables must be declared', () {
      var assignedVariables = AssignedVariablesForTesting<_Node, _Variable>();
      var v1 = _Variable('v1');
      assignedVariables.write(v1);
      expect(assignedVariables.finish, asserts);
    });
  });
}

class _Node {}

class _Variable {
  final String name;

  _Variable(this.name);

  @override
  String toString() => name;
}
