# Copyright 2018 University of Groningen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import copy
import itertools
import numpy as np
import pytest
import hypothesis
import hypothesis.strategies as st
import hypothesis_networkx.strategy as hnst
import vermouth
import vermouth.molecule
from vermouth.molecule import Interaction, Molecule, Block, Link, DeleteInteraction
from .molecule_strategies import attribute_dict, random_molecule, random_block, random_link

# In some cases, drawing from hypothesis.strategies.text is extremely slow and
# triggers the heath check warnings. It appears to be due to some internal
# combinations of timers and cache. This is a workwround that forces the
# generation, and the caching, of the character table out of any test. This
# way, the table is cached, and the time required to build it is not accounted
# in the health checks.
# See <https://github.com/HypothesisWorks/hypothesis/issues/1153>.
hypothesis.internal.charmap.charmap()


@pytest.fixture
def molecule():
    molecule = vermouth.molecule.Molecule()
    molecule.meta['test'] = True
    molecule.meta['test_mutable'] = [0, 1, 2]
    # The node keys should not be in a sorted order as it would mask any issue
    # due to the keys being accidentally sorted.
    molecule.add_node(2, atomname='CC')
    molecule.add_node(0, atomname='AA', mutable=[7, 8, 9])
    molecule.add_node(1, atomname='BB')
    molecule.add_edge(0, 1)
    molecule.add_edge(0, 2)
    molecule.add_interaction(
        type_='bonds',
        atoms=(0, 1),
        parameters=['1', '2'],
        meta={'unmutable': 0, 'mutable': [4, 5, 6]},
    )
    molecule.add_interaction(
        type_='bonds',
        atoms=(0, 2),
        parameters=['a', 'b'],
    )
    return molecule


@pytest.fixture
def molecule_copy(molecule):
    return molecule.copy()


@pytest.mark.parametrize('atoms, bonds, interactions, removed, expected', [
    # empty molecule
    ([], [], [], [], {}),
    # Nodes but no interactions
    ([1, 2, 3], [(1, 2), (2, 3)], [], [2], {}),
    # interactions that all need to be removed
    ([1, 2], [(1, 2)], [('bond', (1, 2), {})], [2], {}),
    # Molecule with interactions of which some need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4), (5, 7)],
        [('bond', (5, 4), {}), ('bond', (1, 2), {})],
        [5],
        {'bond': [vermouth.molecule.Interaction(atoms=(1, 2), meta={}, parameters={})]}
    ),
    # Molecule with interactions of which none need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4)],
        [('bond', (5, 4), {}), ('bond', (1, 2), {})],
        [6, 7],
        {'bond': [vermouth.molecule.Interaction(atoms=(5, 4), meta={}, parameters={}),
                  vermouth.molecule.Interaction(atoms=(1, 2), meta={}, parameters={})]}
    ),
    # Molecule with interactions of different types of which all need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4), (5, 7), (6, 7)],
        [('bond', (1, 6), {}), ('bond', (5, 4), {}), ('angle', (1, 2), {})],
        [1, 5],
        {}
    ),
    # Molecule with interactions of different types of which some need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4), (5, 7), (6, 7)],
        [('bond', (1, 6), {}), ('bond', (5, 4), {}), ('angle', (1, 2), {})],
        [5],
        {'angle': [vermouth.molecule.Interaction(atoms=(1, 2), meta={}, parameters={})],
         'bond': [vermouth.molecule.Interaction(atoms=(1, 6), meta={}, parameters={})]}
    ),
    # Molecule with interactions of different types of which none need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4), (5, 7), (6, 7)],
        [('bond', (1, 6), {}), ('bond', (5, 4), {}), ('angle', (1, 2), {})],
        [3],
        {'angle': [vermouth.molecule.Interaction(atoms=(1, 2), meta={}, parameters={})],
         'bond': [vermouth.molecule.Interaction(atoms=(1, 6), meta={}, parameters={}),
                  vermouth.molecule.Interaction(atoms=(5, 4), meta={}, parameters={})]}
    ),
])
def test_remove_nodes_from(atoms, bonds, interactions, removed, expected):
    """
    Test whether molecule.remove_nodes_from also removes the corresponding
    interactions
    """
    molecule = vermouth.molecule.Molecule()
    molecule.add_nodes_from(atoms)
    molecule.add_edges_from(bonds)
    for type_, atoms, params in interactions:
        molecule.add_interaction(type_, atoms, params)

    molecule.remove_nodes_from(removed)
    assert molecule.interactions == expected


@pytest.mark.parametrize('atoms, bonds, interactions, removed, expected', [
    # empty molecule
    ([], [], [], None, {}),
    # Nodes but no interactions
    ([1, 2, 3], [(1, 2), (2, 3)], [], 2, {}),
    # interactions that all need to be removed
    ([1, 2], [(1, 2)], [('bond', (1, 2), {})], 2, {}),
    # Molecule with interactions of which some need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4), (5, 7)],
        [('bond', (5, 4), {}), ('bond', (1, 2), {})],
        5,
        {'bond': [vermouth.molecule.Interaction(atoms=(1, 2), meta={}, parameters={})]}
    ),
    # Molecule with interactions of different types of which some need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4), (5, 7), (6, 7)],
        [('bond', (1, 6), {}), ('bond', (5, 4), {}), ('angle', (1, 2), {})],
        5,
        {'angle': [vermouth.molecule.Interaction(atoms=(1, 2), meta={}, parameters={})],
         'bond': [vermouth.molecule.Interaction(atoms=(1, 6), meta={}, parameters={})]}
    ),
    # Molecule with interactions of different types of which none need to be removed
    (
        [1, 2, 3, 4, 5, 6, 7],
        [(1, 2), (3, 4), (5, 7), (6, 7)],
        [('bond', (1, 6), {}), ('bond', (5, 4), {}), ('angle', (1, 2), {})],
        3,
        {'angle': [vermouth.molecule.Interaction(atoms=(1, 2), meta={}, parameters={})],
         'bond': [vermouth.molecule.Interaction(atoms=(1, 6), meta={}, parameters={}),
                  vermouth.molecule.Interaction(atoms=(5, 4), meta={}, parameters={})]}
    ),
    (
        [1, 2, 3, 4],
        [(1, 2), (2, 3), (3, 4)],
        [('bond', (1, 2), {}), ('bond', (2, 3), {}), ('bond', (3, 4), {})],
        2,
        {'bond': [vermouth.molecule.Interaction(atoms=(3, 4), meta={}, parameters={})]},
    ),
])
def test_remove_node(atoms, bonds, interactions, removed, expected):
    """
    Test whether molecule.remove_node also removes the corresponding
    interactions
    """
    molecule = vermouth.molecule.Molecule()
    molecule.add_nodes_from(atoms)
    molecule.add_edges_from(bonds)
    for type_, atoms, params in interactions:
        molecule.add_interaction(type_, atoms, params)

    if removed:
        molecule.remove_node(removed)

    assert molecule.interactions == expected

@pytest.fixture
def molecule_subgraph(molecule):
    return molecule.subgraph([2, 0])


def test_copy(molecule, molecule_copy):
    assert molecule_copy is not molecule
    assert molecule_copy.meta == molecule.meta
    assert list(molecule_copy.nodes) == list(molecule.nodes)
    assert list(molecule_copy.nodes.values()) == list(molecule.nodes.values())
    assert molecule_copy.interactions == molecule.interactions


def test_copy_meta_mod(molecule, molecule_copy):
    molecule_copy.meta['test'] = False
    assert molecule_copy.meta['test'] != molecule.meta['test']
    # We are doing a copy, not a deep copy.
    assert molecule_copy.meta['test_mutable'] is molecule.meta['test_mutable']


def test_copy_node_mod(molecule, molecule_copy):
    molecule_copy.nodes[0]['atomname'] = 'mod'
    assert molecule_copy.nodes[0]['atomname'] != molecule.nodes[0]['atomname']
    extra_value = 'a new attribute'
    molecule_copy.nodes[0]['extra'] = extra_value
    assert molecule_copy.nodes[0]['extra'] == extra_value
    assert 'extra' not in molecule.nodes

    # We are looking at a copy, not a deep copy
    assert molecule_copy.nodes[0]['mutable'] is molecule.nodes[0]['mutable']

    molecule_copy.add_node('new')
    assert 'new' in molecule_copy.nodes
    assert 'new' not in molecule


def test_copy_edge_mod(molecule, molecule_copy):
    molecule_copy.add_edge(1, 2)
    assert (1, 2) in molecule_copy.edges
    assert (1, 2) not in molecule.edges
    molecule_copy.edges[(0, 1)]['attribute'] = 1
    assert molecule_copy.edges[(0, 1)]['attribute'] == 1
    assert 'attribute' not in molecule.edges[(0, 1)]


def test_copy_interactions_mod(molecule, molecule_copy):
    molecule_copy.add_interaction(
        type_='bonds',
        atoms=(0, 2),
        parameters=['3', '4'],
        meta={'unmutable': 0},
    )
    n_bonds = len(molecule.interactions['bonds'])
    n_bonds_copy = len(molecule_copy.interactions['bonds'])
    assert n_bonds_copy > n_bonds


def test_subgraph_base(molecule_subgraph):
    assert tuple(molecule_subgraph) == (2, 0)  # order matters!
    assert (0, 2) in molecule_subgraph.edges
    assert (0, 1) not in molecule_subgraph.edges  # node 1 is not there


def test_subgraph_interactions(molecule_subgraph):
    bond_atoms = [bond.atoms for bond in molecule_subgraph.interactions['bonds']]
    assert (0, 2) in bond_atoms
    assert (0, 1) not in bond_atoms


def test_link_predicate_match():
    lp = vermouth.molecule.LinkPredicate(None)
    with pytest.raises(NotImplementedError):
        lp.match(1, 2)


@pytest.fixture
def edges_between_molecule():
    """
    Build an empty molecule with known connectivity.

    The molecule does not have any node attribute nor any molecule metadata. It
    only has a bare graph with a few nodes and edges.

    The graph looks like::

        0 - 1 - 3 - 4 - 5 - 7 - 8     9 - 10 - 11 - 12
            |           |
            2           6

    """
    molecule = vermouth.molecule.Molecule()
    molecule.add_edges_from((
        (0, 1), (1, 2), (1, 3), (3, 4), (4, 5), (5, 6), (5, 7), (7, 8),
        (9, 10), (10, 11), (11, 12),
    ))
    for node1, node2, attributes in molecule.edges(data=True):
        attributes['arbitrary'] = '{} - {}'.format(min(node1, node2),
                                                   max(node1, node2))
    return molecule


@pytest.fixture
def edges_between_selections():
    """
    Build a static list of selections of nodes from :func:`edges_between_molecule`.
    """
    return [
        (0, 1, 2, 3),
        (5, 6, 7, 8),
        (9, 10, 11, 12),
        (3, 4, 5, 6),
        (7, 8, 9, 10),
    ]


@pytest.mark.parametrize('data', [True, False])
@pytest.mark.parametrize('bunch1, bunch2, expected', (
    (0, 1, []), (0, 2, []), (0, 4, []), (1, 2, []), (2, 3, []),  # non-overlapping
    (0, 3, [(1, 3), (3, 4)]), (1, 3, [(4, 5), (5, 6), (5, 6), (5, 7)]),
    (1, 4, [(5, 7), (7, 8), (7, 8)]), (2, 4, [(9, 10), (9, 10), (10, 11)]),
))
def test_edges_between(edges_between_molecule, edges_between_selections,
                       bunch1, bunch2, expected, data):
    """
    Test :meth:`vermouth.molecule.Molecule.edges_between`.
    """
    selection_1 = edges_between_selections[bunch1]
    selection_2 = edges_between_selections[bunch2]
    found = list(edges_between_molecule.edges_between(
        selection_1, selection_2, data=data
    ))
    sorted_found = sorted(sorted(edge[:2]) for edge in found)
    sorted_expected = sorted(sorted(edge) for edge in expected)
    assert sorted_found == sorted_expected
    if data:
        found_attributes = [
            edge[2]
            for edge in sorted(found, key=lambda x: x[:2])
        ]
        expected_attributes = [
            edges_between_molecule.edges[edge[0], edge[1]]
            for edge in sorted_expected
        ]
        assert found_attributes == expected_attributes



@pytest.mark.parametrize('selidx, expected', (
    (0, ((0, 1), (1, 2), (1, 3))),
    (1, ((5, 6), (5, 7), (7, 8))),
    (2, ((9, 10), (10, 11), (11, 12))),
    (3, ((3, 4), (4, 5), (5, 6))),
    (4, ((7, 8), (9, 10))),
))
def test_subgraph_edges(edges_between_molecule, edges_between_selections,
                        selidx, expected):
    """
    :meth:`vermouth.molecule.Molecule.subgraph` select the expected edges.

    See Also
    --------
    test_subgraph_base
        This test deals with a larger graph, but no metadata; while the graph
        in :func:`test_subgraph_base` uses a very small and simple selection
        but the graph has metadata.
    """
    subgraph = edges_between_molecule.subgraph(edges_between_selections[selidx])
    sorted_found = sorted(sorted(edge) for edge in subgraph.edges)
    sorted_expected = sorted(sorted(edge) for edge in expected)
    assert sorted_found == sorted_expected

    found_attributes = [
        subgraph.edges[edge[0], edge[1]]
        for edge in sorted_expected
    ]
    expected_attributes = [
        edges_between_molecule.edges[edge[0], edge[1]]
        for edge in sorted_expected
    ]
    assert found_attributes == expected_attributes


@pytest.mark.parametrize('left, right, expected', (
    (  # Same
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        True,
    ),
    (  # Difference in atoms
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'notC'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        False,
    ),
    (  # Difference in parameters
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['different'],
                            meta={'a': 0}),
            ],
        },
        False,
    ),
    (  # Difference in meta
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=['a', '0.2', '200'],
                            meta={'a': 0, 'other': True}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        False,
    ),
    (  # Equal with LinkParameterEffector
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamDistance(['A', 'B']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamDistance(['A', 'B']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        True,
    ),
    (  # Different arguments for LinkParameterEffector
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamDistance(['A', 'B']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamDistance(['A', 'C']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        False,
    ),
    (  # Different format_spec in LinkParameterEffector
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamDistance(['A', 'B']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamDistance(
                                    ['A', 'B'], format_spec='.2f',
                                ),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        False,
    ),
    (  # Different LinkParameterEffector
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamDistance(['A', 'B']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamAngle( ['A', 'B', 'C']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        False,
    ),
    (  # Missing interaction category ("angles" is missing in one molecule)
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamAngle( ['A', 'B', 'C']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [
                Interaction(atoms=('A', 'B', 'C'),
                            parameters=['1', '0.2', '200'],
                            meta={'a': 0}),
            ],
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamAngle( ['A', 'B', 'C']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
        },
        False,
    ),
    (  # Empty interaction category
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamAngle( ['A', 'B', 'C']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
            'angles': [],  # Empty!
        },
        {
            'bonds': [
                Interaction(atoms=('A', 'B'),
                            parameters=[
                                'a',
                                vermouth.molecule.ParamAngle( ['A', 'B', 'C']),
                                '200',
                            ],
                            meta={'a': 0}),
                Interaction(atoms=('B', 'C'),
                            parameters=['a', '0.1', '300'],
                            meta={'b': 1}),
            ],
        },
        True,
    ),

))
def test_same_interactions(left, right, expected):
    """
    Test that Molecule.same_interactions works as expected.
    """
    left_mol = Molecule()
    left_mol.interactions = left
    right_mol = Molecule()
    right_mol.interactions = right
    assert left_mol.same_interactions(right_mol) == expected
    assert right_mol.same_interactions(left_mol) == expected


@pytest.mark.parametrize('left, right, expected', (
    (  # Simple identical
        (  # left
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        True,  # expected
    ),
    (  # Wrong order
        (  # left
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (1, {'c': (0, 1, 2), 'd': None}),
            (0, {'a': 'abc', 'b': 123}),
        ),
        False,  # expected
    ),

    (  # Different string
        (  # left
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': 'different', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        False,
    ),
    (  # Different number
        (  # left
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': 'abc', 'b': 900}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        False,  # expected
    ),
    (  # Different tuple
        (  # left
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (3, 2, 1), 'd': None}),
        ),
        False,  # expected
    ),
    (  # Equal Numpy array
        (  # left
            (0, {'a': np.linspace(2, 5, num=7), 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': np.linspace(2, 5, num=7), 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        True,  # expected
    ),
    (  # Different Numpy array
        (  # left
            (0, {'a': np.linspace(2, 5, num=7), 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': np.linspace(2, 8, num=7), 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        False,  # expected
    ),
    (  # Different shaped Numpy array
        (  # left
            (0, {'a': np.linspace(2, 5, num=9), 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': np.linspace(2, 5, num=9).reshape((3, 3)), 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        False,  # expected
    ),
    (  # Mismatch types
        (  # left
            (0, {'a': np.linspace(2, 5, num=9), 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': 'not an array', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        False,  # expected
    ),
    (  # Mismatch attribute key
        (  # left
            (0, {'a': 'abc', 'b': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        (  # right
            (0, {'a': 'abc', 'different': 123}),
            (1, {'c': (0, 1, 2), 'd': None}),
        ),
        False,  # expected
    ),
    (  # same dicts as value
        (  # left
            (0, {'a': {'a': 0, 'b': 1}}),
        ),
        (  # right
            (0, {'a': {'a': 0, 'b': 1}}),
        ),
        True,
    ),
    (  # different dicts as value
        (  # left
            (0, {'a': {'a': 0, 'b': 1}}),
        ),
        (  # right
            (0, {'a': {'a': None, 'b': 1}}),
        ),
        False,
    ),
))
def test_same_nodes(left, right, expected):
    left_mol = Molecule()
    left_mol.add_nodes_from(left)
    right_mol = Molecule()
    right_mol.add_nodes_from(right)
    assert left_mol.same_nodes(right_mol) == expected
    assert right_mol.same_nodes(left_mol) == expected


@pytest.mark.parametrize('effector_class', (
    vermouth.molecule.ParamDistance,
    vermouth.molecule.ParamAngle,
    vermouth.molecule.ParamDihedral,
    vermouth.molecule.ParamDihedralPhase,
))
@pytest.mark.parametrize('format_spec', (
    None, '.2f', '0.3f',
))
def test_link_parameter_effector_equal(effector_class, format_spec):
    """
    Test that equal LinkParameterEffector compare equal.
    """
    n_keys = effector_class.n_keys_asked
    left_keys = ['A{}'.format(idx) for idx in range(n_keys)]
    right_keys = copy.copy(left_keys)  # Let's be sure the id is different
    left = effector_class(left_keys, format_spec=format_spec)
    right = effector_class(right_keys, format_spec=format_spec)
    assert left == right


@pytest.mark.parametrize('effector_class', (
    vermouth.molecule.ParamDistance,
    vermouth.molecule.ParamAngle,
    vermouth.molecule.ParamDihedral,
    vermouth.molecule.ParamDihedralPhase,
))
@pytest.mark.parametrize('format_right, format_left', itertools.combinations(
    (None, '.2f', '0.3f', ), 2
))
def test_link_parameter_effector_diff_format(effector_class, format_left, format_right):
    """
    Test that two instances of LinkParameterEffector compare different if they
    have different formats.
    """
    n_keys = effector_class.n_keys_asked
    left_keys = ['A{}'.format(idx) for idx in range(n_keys)]
    right_keys = copy.copy(left_keys)  # Let's be sure the id is different
    left = effector_class(left_keys, format_spec=format_left)
    right = effector_class(right_keys, format_spec=format_right)
    assert left != right


@pytest.mark.parametrize('effector_class', (
    vermouth.molecule.ParamDistance,
    vermouth.molecule.ParamAngle,
    vermouth.molecule.ParamDihedral,
    vermouth.molecule.ParamDihedralPhase,
))
def test_link_parameter_effector_diff_keys(effector_class):
    """
    Test that LinkParameterEffector compare different if they have different keys.
    """
    n_keys = effector_class.n_keys_asked
    left_keys = ['A{}'.format(idx) for idx in range(n_keys)]
    right_keys = ['B{}'.format(idx) for idx in range(n_keys)]
    left = effector_class(left_keys)
    right = effector_class(right_keys)
    assert left != right


@pytest.mark.parametrize('left_class, right_class', itertools.combinations((
    vermouth.molecule.ParamDistance,
    vermouth.molecule.ParamAngle,
    vermouth.molecule.ParamDihedral,
    vermouth.molecule.ParamDihedralPhase,
), 2))
def test_link_parameter_effector_diff_class(left_class, right_class):
    """
    Test that LinkParameterEffector compare different if they have different classes.
    """
    left_n_keys = left_class.n_keys_asked
    left_keys = ['A{}'.format(idx) for idx in range(left_n_keys)]
    left = left_class(left_keys)

    right_n_keys = right_class.n_keys_asked
    right_keys = ['A{}'.format(idx) for idx in range(right_n_keys)]
    right = right_class(right_keys)

    assert left != right


@hypothesis.settings(suppress_health_check=[hypothesis.HealthCheck.too_slow])
@hypothesis.given(random_molecule())
def test_molecule_equal(mol):
    """
    Two equal molecules are equal.
    """
    mol_copy = mol.copy()
    assert mol == mol_copy
    assert mol is not mol_copy


@hypothesis.given(random_block())
def test_block_equal(block):
    """
    Two equal blocks are equal.
    """
    block_copy = block.copy()
    assert block == block_copy
    assert block is not block_copy


@hypothesis.given(random_link(max_nodes=3, max_meta=3))
def test_link_equal(link):
    """
    Two equal links are equal.
    """
    link_copy = link.copy()
    assert link == link
    assert link is not link_copy


@pytest.mark.parametrize('left, right, expected', (
    (  # Equal edges, same order, same direction, same attributes
        [
            (0, 1, {'a': 0, 'b': [2, 3]}),
            (1, 3, {'foo': 'bar'}),
        ],
        [
            (0, 1, {'a': 0, 'b': [2, 3]}),
            (1, 3, {'foo': 'bar'}),
        ],
        True,
    ),
    (  # Equal edges, but different order
        [
            (0, 1, {'a': 0, 'b': [2, 3]}),
            (1, 3, {'foo': 'bar'}),
        ],
        [
            (1, 3, {'foo': 'bar'}),
            (0, 1, {'a': 0, 'b': [2, 3]}),
        ],
        True,
    ),
    (  # Equal edges, but different order and different direction
        [
            (0, 1, {'a': 0, 'b': [2, 3]}),
            (1, 3, {'foo': 'bar'}),
        ],
        [
            (3, 1, {'foo': 'bar'}),
            (0, 1, {'a': 0, 'b': [2, 3]}),
        ],
        True,
    ),
    (  # Edges only differ by an attribute
        [
            (0, 1, {'a': 0, 'b': [2, 3]}),
            (1, 3, {'foo': 'bar'}),
        ],
        [
            (0, 1, {'a': 0, 'b': [2, 40]}),
            (1, 3, {'foo': 'bar'}),
        ],
        False,
    ),
    (  # Edges are different
        [
            (0, 1, {'foo': 'bar'}),
            (2, 3, {}),
        ],
        [
            (5, 6, {'foo': 'bar'}),
            (3, 4, {}),
        ],
        False,
    ),
))
def test_same_edges(left, right, expected):
    """
    Compare edges between two molecules.
    """
    molecule_left = vermouth.molecule.Molecule()
    molecule_left.add_edges_from(left)
    molecule_right = vermouth.molecule.Molecule()
    molecule_right.add_edges_from(right)
    assert molecule_left.same_edges(molecule_right) == expected
    assert molecule_right.same_edges(molecule_left) == expected


@pytest.mark.parametrize('left, right, expected', (
    (  # Identical non-edges
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        True,
    ),
    (  # Identical non-edges, but disordered
        [
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
        ],
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        True,
    ),

    (  # Different number of non-edges
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('YY', {'klonk': 0}),
        ],
        False,
    ),
    (  # Same number of non-edges, but mismatched anchors
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        [
            ('AA', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('BB', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        False,
    ),
    (  # Mismatch in an attribute
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        [
            ('XX', {'foo': 'mismatch', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        False,
    ),
    (  # Mismatch in number of attributes, there is a shortcut in same_non_edges
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar'}),
            ('YY', {}),
        ],
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar', 'plop': 'else'}),
            ('YY', {'klonk': 0}),
        ],
        False,
    ),
    (
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar'}),
            ('XX', {'foo': 'no bar again'}),
            ('YY', {}),
        ],
        [
            ('XX', {'foo': 'bar', 'toto': [0, 1, 2]}),
            ('XX', {'foo': 'no bar'}),
            ('YY', {}),
            ('YY', {'something': 'tadaam'}),
        ],
        False,
    ),
))
def test_same_non_edges(left, right, expected):
    """
    Compare non-edges between two links.
    """
    link_left = vermouth.molecule.Link()
    link_left.non_edges = left
    link_right = vermouth.molecule.Link()
    link_right.non_edges = right
    assert link_left.same_non_edges(link_right) == expected
    assert link_right.same_non_edges(link_left) == expected



@pytest.mark.parametrize('interactions, expected', (
    (
        {},
        []
    ),
    (
        {'bonds': []},
        []
    ),
    (
        {'bonds': [Interaction(atoms=(1, 2), parameters=[], meta=[])]},
        ['bonds']
    ),
    (
        {'bonds': [Interaction(atoms=(1, 2), parameters=[], meta=[])],
         'angles': []},
        ['bonds']
    ),
    (
        {'bonds': [Interaction(atoms=(1, 2), parameters=[], meta=[])],
         'angles': [Interaction(atoms=(1, 2, 3), parameters=[], meta=[])]},
        ['bonds', 'angles']
    ),
    (
        {'angles': [Interaction(atoms=(1, 2, 3), parameters=[], meta=[])],
         'bonds': [Interaction(atoms=(1, 2), parameters=[], meta=[])],},
        ['bonds', 'angles']
    ),
    (
        {'angles': [Interaction(atoms=(1, 2, 3), parameters=[], meta=[])],
         'a_bonds': [Interaction(atoms=(10, 2), parameters=[], meta=[])],
         'bonds': [Interaction(atoms=(1, 2), parameters=[], meta=[])],},
        ['a_bonds', 'bonds', 'angles']
    ),
))
def test_interaction_sort(interactions, expected):
    """
    Test the order produced by Molecule._sort_interactions.
    """
    assert vermouth.molecule.Molecule._sort_interactions(interactions) == expected


@hypothesis.given(moltype=st.one_of(st.none(), st.text()), mol=random_molecule())
def test_str_method(mol, moltype):
    """
    Test Molecule.__str__
    """
    if moltype is not None:
        mol.meta['moltype'] = moltype
    else: 
        moltype = 'molecule'
    found = str(mol)
    assert '{} with '.format(moltype) in found
    assert '{} {}'.format(len(mol), 'atoms') in found
    hypothesis.assume('' not in mol.interactions)
    for itype, interactions in mol.interactions.items():
        if interactions:
            assert '{} {}'.format(len(interactions), itype) in found
        else:
            assert '{} {}'.format(0, itype) not in found
