"""Tests for the transformers module.

These tests rely heavily on fixtures defined in conftest.py.
"""
import ast
import sys

from copy import deepcopy

import pytest

from mutatest.api import Genome
from mutatest.transformers import LocIndex, MutateAST, get_mutations_for_target


TEST_BINOPS = {ast.Add, ast.Sub, ast.Div, ast.Mult, ast.Pow, ast.Mod, ast.FloorDiv}


@pytest.mark.parametrize("test_op", TEST_BINOPS)
def test_get_mutations_for_target(test_op):
    """Ensure the expected set is returned for binops"""
    mock_loc_idx = LocIndex(ast_class="BinOp", lineno=10, col_offset=11, op_type=test_op)

    expected = TEST_BINOPS.copy()
    expected.remove(test_op)

    result = get_mutations_for_target(mock_loc_idx)
    assert result == expected


def test_MutateAST_visit_read_only(binop_file):
    """Read only test to ensure locations are aggregated."""
    tree = Genome(binop_file).ast
    mast = MutateAST(readonly=True)
    testing_tree = deepcopy(tree)
    mast.visit(testing_tree)

    # four locations from the binary operations in binop_file
    assert len(mast.locs) == 4

    # tree should be unmodified
    assert ast.dump(tree) == ast.dump(testing_tree)


####################################################################################################
# GENERIC TRANSFORMER NODE TESTS
# These represent the basic pattern for visiting a node in the MutateAST class and applying a
# mutation without running the full test suite against the cached files.
####################################################################################################


def test_MutateAST_visit_augassign(augassign_file, augassign_expected_locs):
    """Test mutation for AugAssign: +=, -=, /=, *=."""
    tree = Genome(augassign_file).ast
    test_mutation = "AugAssign_Div"

    testing_tree = deepcopy(tree)
    mutated_tree = MutateAST(target_idx=augassign_expected_locs[0], mutation=test_mutation).visit(
        testing_tree
    )

    mast = MutateAST(readonly=True)
    mast.visit(mutated_tree)

    assert len(mast.locs) == 4

    for loc in mast.locs:
        # spot check on mutation from Add tp Div
        if loc.lineno == 1 and loc.col_offset == 4:
            assert loc.op_type == test_mutation

        # spot check on not-mutated location still being Mult
        if loc.lineno == 5 and loc.col_offset == 4:
            assert loc.op_type == "AugAssign_Mult"


def test_MutateAST_visit_binop_37(binop_file):
    """Read only test to ensure locations are aggregated."""
    tree = Genome(binop_file).ast

    # Py 3.7 vs. Py 3.8
    end_lineno = None if sys.version_info < (3, 8) else 6
    end_col_offset = None if sys.version_info < (3, 8) else 17

    test_idx = LocIndex(
        ast_class="BinOp",
        lineno=6,
        col_offset=11,
        op_type=ast.Add,
        end_lineno=end_lineno,
        end_col_offset=end_col_offset,
    )
    test_mutation = ast.Pow

    # apply the mutation to the original tree copy
    testing_tree = deepcopy(tree)
    mutated_tree = MutateAST(target_idx=test_idx, mutation=test_mutation).visit(testing_tree)

    # revisit in read-only mode to gather the locations of the new nodes
    mast = MutateAST(readonly=True)
    mast.visit(mutated_tree)

    # four locations from the binary operations in binop_file
    assert len(mast.locs) == 4

    # locs is an unordered set, cycle through to thd target and check the mutation
    for loc in mast.locs:
        if (
            loc.lineno == 6
            and loc.col_offset == 11
            and loc.end_lineno == end_lineno
            and loc.end_col_offset == end_col_offset
        ):
            assert loc.op_type == test_mutation


def test_MutateAST_visit_boolop(boolop_file, boolop_expected_loc):
    """Test mutation of AND to OR in the boolop."""
    tree = Genome(boolop_file).ast
    test_mutation = ast.Or

    # apply the mutation to the original tree copy
    testing_tree = deepcopy(tree)
    mutated_tree = MutateAST(target_idx=boolop_expected_loc, mutation=test_mutation).visit(
        testing_tree
    )

    # revisit in read-only mode to gather the locations of the new nodes
    mast = MutateAST(readonly=True)
    mast.visit(mutated_tree)

    # four locations from the binary operations in binop_file
    assert len(mast.locs) == 1

    # there will only be one loc, but this still works
    # basedon the col and line offset in the fixture for compare_expected_loc
    for loc in mast.locs:
        if loc.lineno == 2 and loc.col_offset == 11:
            assert loc.op_type == test_mutation


@pytest.mark.parametrize(  # based on the fixture definitions for compare_file and expected_locs
    "idx, mut_op, lineno",
    [(0, ast.NotEq, 2), (1, ast.IsNot, 5), (2, ast.NotIn, 8)],
    ids=["Compare", "CompareIs", "CompareIn"],
)
def test_MutateAST_visit_compare(idx, mut_op, lineno, compare_file, compare_expected_locs):
    """Test mutation of the == to != in the compare op."""
    tree = Genome(compare_file).ast

    # apply the mutation to the original tree copy
    testing_tree = deepcopy(tree)
    mutated_tree = MutateAST(target_idx=compare_expected_locs[idx], mutation=mut_op).visit(
        testing_tree
    )

    # revisit in read-only mode to gather the locations of the new nodes
    mast = MutateAST(readonly=True)
    mast.visit(mutated_tree)

    assert len(mast.locs) == 3

    # check that the lineno marked for mutation is changed, otherwise original ops should
    # still be present without modification
    for loc in mast.locs:
        if loc.lineno == lineno and loc.col_offset == 11:
            assert loc.op_type == mut_op
        else:
            assert loc.op_type in {ast.Eq, ast.Is, ast.In}  # based on compare_file fixture


def test_MutateAST_visit_if(if_file, if_expected_locs):
    """Test mutation for nameconst: True, False, None."""
    tree = Genome(if_file).ast
    test_mutation = "If_True"

    testing_tree = deepcopy(tree)
    # change from If_Statement to If_True
    mutated_tree = MutateAST(target_idx=if_expected_locs[0], mutation=test_mutation).visit(
        testing_tree
    )

    mast = MutateAST(readonly=True)
    mast.visit(mutated_tree)

    # named constants will also be picked up, filter just to if_ operations
    if_locs = [loc for loc in mast.locs if loc.ast_class == "If"]
    assert len(if_locs) == 4

    for loc in if_locs:
        # spot check on mutation from True to False
        if loc.lineno == 2 and loc.col_offset == 4:
            assert loc.op_type == test_mutation

        # spot check on not-mutated location still being None
        if loc.lineno == 13 and loc.col_offset == 4:
            assert loc.op_type == "If_False"


INDEX_SETS = [
    # idx order, lineno, col_offset, mutation to apply
    # change NumNeg to Pos and Zero
    (0, 2, 20, "Index_NumPos"),
    (0, 2, 20, "Index_NumZero"),
    # change NumZero to Neg and Pos
    (1, 3, 20, "Index_NumNeg"),
    (1, 3, 20, "Index_NumPos"),
    # chang NumPos to Neg and Zero
    (2, 4, 20, "Index_NumNeg"),
    (2, 4, 20, "Index_NumZero"),
]


@pytest.mark.parametrize(
    "i_order, lineno, col_offset, mut",
    INDEX_SETS,
    ids=[
        "NumNeg to NumPos",
        "NumNeg to NumZero",
        "NumZero to NumNeg",
        "NumZero to NumPos",
        "NumPos to NumNeg",
        "NumPos to NumZero",
    ],
)
def test_MutateAST_visit_index_neg(
    i_order, lineno, col_offset, mut, index_file, index_expected_locs
):
    """Test mutation for Index: i[0], i[1], i[-1]."""
    tree = Genome(index_file).ast
    test_mutation = mut

    testing_tree = deepcopy(tree)
    mutated_tree = MutateAST(target_idx=index_expected_locs[i_order], mutation=test_mutation).visit(
        testing_tree
    )

    mast = MutateAST(readonly=True)
    mast.visit(mutated_tree)

    assert len(mast.locs) == 4

    for loc in mast.locs:
        # spot check on mutation from Index_NumNeg to Index_NumPos
        if loc.lineno == lineno and loc.col_offset == col_offset:
            assert loc.op_type == test_mutation

        # spot check on not-mutated location still being None
        if loc.lineno == 4 and loc.col_offset == 23:
            assert loc.op_type == "Index_NumPos"


def test_MutateAST_visit_nameconst(nameconst_file, nameconst_expected_locs):
    """Test mutation for nameconst: True, False, None."""
    tree = Genome(nameconst_file).ast
    test_mutation = False

    testing_tree = deepcopy(tree)
    mutated_tree = MutateAST(target_idx=nameconst_expected_locs[0], mutation=test_mutation).visit(
        testing_tree
    )

    mast = MutateAST(readonly=True)
    mast.visit(mutated_tree)

    # if statement is included with this file that will be picked up
    nc_locs = [loc for loc in mast.locs if loc.ast_class == "NameConstant"]
    assert len(nc_locs) == 4

    for loc in nc_locs:
        # spot check on mutation from True to False
        if loc.lineno == 1 and loc.col_offset == 14:
            assert loc.op_type == test_mutation

        # spot check on not-mutated location still being None
        if loc.lineno == 7 and loc.col_offset == 22:
            assert loc.op_type is None


def test_MutateAST_visit_subscript(slice_file, slice_expected_locs):
    """Test Slice references within subscript."""
    tree = Genome(slice_file).ast
    mast = MutateAST(readonly=True)
    mast.visit(tree)
    assert len(mast.locs) == len(slice_expected_locs)

    test_mutation = "Slice_UNegToZero"

    mutated_tree = MutateAST(target_idx=slice_expected_locs[2], mutation=test_mutation).visit(tree)

    mast.visit(mutated_tree)
    assert len(mast.locs) == len(slice_expected_locs)

    for loc in mast.locs:

        if loc.lineno == 5 and loc.col_offset == 15:
            assert loc.op_type == test_mutation

        # test one unmodified location
        if loc.lineno == 4 and loc.col_offset == 14:
            assert loc.op_type == "Slice_UnboundUpper"