"""Test configuration, large and shared fixtures.
"""
import ast
import contextlib
import sys

from datetime import timedelta
from io import StringIO
from operator import attrgetter
from pathlib import Path
from textwrap import dedent
from typing import Dict, List, NamedTuple, Set, Optional

import coverage
import pytest

from mutatest.api import Mutant
from mutatest.run import MutantTrialResult, ResultsSummary
from mutatest.transformers import LocIndex


class FileAndTest(NamedTuple):
    """Container for paired file and test location in tmp_path_factory fixtures."""

    src_file: Path
    test_file: Path


####################################################################################################
# GENERIC FIXTURES FOR MUTANTS
####################################################################################################


@pytest.fixture(scope="session")
def stdoutIO():
    """Stdout redirection as a context manager to evaluating code mutations."""

    @contextlib.contextmanager
    def stdoutIO(stdout=None):
        old = sys.stdout
        if stdout is None:
            stdout = StringIO()
        sys.stdout = stdout
        yield stdout
        sys.stdout = old

    return stdoutIO


@pytest.fixture(scope="session")
def mock_Mutant():
    """Mock mutant definition."""
    return Mutant(
        mutant_code=None,
        src_file=Path("src.py"),
        cfile=Path("__pycache__") / "src.pyc",
        loader=None,
        mode=1,
        source_stats={"mtime": 1, "size": 1},
        src_idx=LocIndex(ast_class="BinOp", lineno=1, col_offset=2, op_type=ast.Add),
        mutation=ast.Mult,
    )


@pytest.fixture(scope="session")
def mock_trial_results(mock_Mutant):
    """Mock mutant trial results for each status code."""
    return [
        MutantTrialResult(mock_Mutant, return_code=0),  # SURVIVED
        MutantTrialResult(mock_Mutant, return_code=1),  # DETECTED
        MutantTrialResult(mock_Mutant, return_code=2),  # ERROR
        MutantTrialResult(mock_Mutant, return_code=3),  # TIMEOUT
        MutantTrialResult(mock_Mutant, return_code=4),  # UNKNOWN
    ]


@pytest.fixture(scope="session")
def mock_results_summary(mock_trial_results):
    """Mock results summary from multiple trials."""
    return ResultsSummary(
        results=mock_trial_results,
        n_locs_identified=4,
        n_locs_mutated=4,
        total_runtime=timedelta(days=0, seconds=6, microseconds=0),
    )


def write_cov_file(line_data: Dict[str, List[int]], fname: str) -> None:
    """Write a coverage file supporting both Coverage v4 and v5.

    Args:
        line_data: Dictionary of line data for the coverage file.
        fname: string filename for output location (absolute path)

    Returns:
        None
    """
    if coverage.version_info[0] == 4:
        covdata = coverage.CoverageData()
        covdata.add_lines(line_data)
        covdata.write_file(fname)

    else:
        # assume coverage v 5
        covdata = coverage.CoverageData(basename=fname)
        covdata.add_lines(line_data)
        covdata.write()


####################################################################################################
# CLI: MOCK ARGS
####################################################################################################


class MockArgs(NamedTuple):
    """Container for mocks of the cli arguments."""

    blacklist: Optional[List[str]]
    exclude: Optional[List[str]]
    mode: Optional[str]
    nlocations: Optional[int]
    output: Optional[Path]
    rseed: Optional[int]
    src: Optional[Path]
    testcmds: Optional[List[str]]
    whitelist: Optional[List[str]]
    exception: Optional[int]
    debug: Optional[bool]
    nocov: Optional[bool]
    parallel: Optional[bool]
    timeout_factor: Optional[int]


@pytest.fixture(scope="session")
def mock_args(tmp_path_factory, binop_file):
    """Basic fixture with default settings using existing binop_file fixture."""

    folder = tmp_path_factory.mktemp("output")

    return MockArgs(
        blacklist=[],
        exclude=["__init__.py"],
        mode="s",
        nlocations=10,
        output=folder / "mock_mutation_report.rst",
        rseed=314,
        src=binop_file,
        testcmds=["pytest"],
        whitelist=[],
        exception=None,
        debug=False,
        nocov=True,
        parallel=False,
        timeout_factor=2,
    )


####################################################################################################
# FILTERS: MOCK COVERAGE FILE FIXTURES
####################################################################################################


@pytest.fixture(scope="session")
def mock_coverage_file(tmp_path_factory):
    """Mock .coverage file to read into the CoverageOptimizer."""

    folder = tmp_path_factory.mktemp("cov")

    # aligned to fixture mock_source_and_targets for file3.py used in positive filter.
    mock_contents = {
        "file1.py": [1],
        "file2.py": [1, 3, 4],
        "file3.py": [1, 2, 4],
    }

    mock_cov_file = folder / ".coverage"
    str_cov_file = str(mock_cov_file.resolve())
    write_cov_file(mock_contents, str_cov_file)

    yield mock_cov_file

    mock_cov_file.unlink()


class SourceAndTargets(NamedTuple):
    """Container for use with Coverage Filter mock sets."""

    source_file: Path
    targets: Set[LocIndex]


@pytest.fixture(scope="session")
def mock_source_and_targets():
    """Mock source file with uncovered/covered targets to use with mock_coverage_file.

    Covered lines include: 1, 2, 4
    """
    # see mock_coverage_file fixture
    source_file = Path("file3.py")
    targets = {
        LocIndex(ast_class="AugAssign", lineno=1, col_offset=1, op_type="o"),
        LocIndex(ast_class="AugAssign", lineno=2, col_offset=1, op_type="o"),
        LocIndex(ast_class="AugAssign", lineno=3, col_offset=1, op_type="o"),
        LocIndex(ast_class="BinOp", lineno=4, col_offset=1, op_type="o"),
        LocIndex(ast_class="BinOp", lineno=5, col_offset=1, op_type="o"),
    }
    return SourceAndTargets(source_file, targets)


####################################################################################################
# TRANSFORMERS: AUGASSIGN FIXTURES
####################################################################################################


@pytest.fixture(scope="session")
def augassign_file(tmp_path_factory):
    """A simple python file with the AugAssign attributes."""
    contents = dedent(
        """\
    def my_func(a, b):
        a += 6
        b -= 4
        b /= 2
        b *= 3

        return a, b
    """
    )

    fn = tmp_path_factory.mktemp("augassign") / "augassign.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def augassign_expected_locs():
    """The AugAssign expected location based on the fixture"""
    return [
        LocIndex(ast_class="AugAssign", lineno=2, col_offset=4, op_type="AugAssign_Add"),
        LocIndex(ast_class="AugAssign", lineno=3, col_offset=4, op_type="AugAssign_Sub"),
        LocIndex(ast_class="AugAssign", lineno=4, col_offset=4, op_type="AugAssign_Div"),
        LocIndex(ast_class="AugAssign", lineno=5, col_offset=4, op_type="AugAssign_Mult"),
    ]


####################################################################################################
# TRANSFORMERS: BINOP FIXTURES
# Used as a baseline fixture in many running tests
####################################################################################################


@pytest.fixture(scope="session")
def binop_file(tmp_path_factory):
    """A simple python file with binary operations."""
    contents = dedent(
        """\
    def myfunc(a):
        print("hello", a)


    def add_ten(b):
        return b + 11 - 1


    def add_five(b):
        return b + 5


    def add_five_divide_3(b):
        x = add_five(b)
        return x / 3

    print(add_five(5))
    """
    )

    fn = tmp_path_factory.mktemp("binops") / "binops.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def mock_binop_coverage_file(binop_file, tmp_path_factory):
    """Mock .coverage file based on the binop_file fixture."""
    fname = str(binop_file.resolve())
    mock_contents = {fname: [6, 10]}

    folder = tmp_path_factory.mktemp("binop_cov")
    mock_cov_file = folder / ".coverage"
    str_cov_file = str(mock_cov_file.resolve())
    write_cov_file(mock_contents, str_cov_file)

    yield mock_cov_file

    mock_cov_file.unlink()


@pytest.fixture(scope="session")
def binop_expected_locs():
    """Expected target locations for the binop_file fixture in Python 3.7."""
    # Python 3.7
    if sys.version_info < (3, 8):
        return {
            LocIndex(ast_class="BinOp", lineno=6, col_offset=11, op_type=ast.Add),
            LocIndex(ast_class="BinOp", lineno=6, col_offset=18, op_type=ast.Sub),
            LocIndex(ast_class="BinOp", lineno=10, col_offset=11, op_type=ast.Add),
            LocIndex(ast_class="BinOp", lineno=15, col_offset=11, op_type=ast.Div),
        }

    # Python 3.8
    return {
        LocIndex(
            ast_class="BinOp",
            lineno=15,
            col_offset=11,
            op_type=ast.Div,
            end_lineno=15,
            end_col_offset=16,
        ),
        LocIndex(
            ast_class="BinOp",
            lineno=6,
            col_offset=11,
            op_type=ast.Add,
            end_lineno=6,
            end_col_offset=17,
        ),
        LocIndex(
            ast_class="BinOp",
            lineno=10,
            col_offset=11,
            op_type=ast.Add,
            end_lineno=10,
            end_col_offset=16,
        ),
        LocIndex(
            ast_class="BinOp",
            lineno=6,
            col_offset=11,
            op_type=ast.Sub,
            end_lineno=6,
            end_col_offset=21,
        ),
    }


@pytest.fixture(scope="session")
def sorted_binop_expected_locs(binop_expected_locs):
    """Sorted expected locs when used in tests for sample generation."""
    sort_by = attrgetter("lineno", "col_offset", "end_lineno", "end_col_offset")
    return sorted(binop_expected_locs, key=sort_by)


@pytest.fixture(scope="session")
def mock_LocIdx():
    """Mock Single Location Index, not a valid target member of file."""
    return LocIndex(ast_class="BinOp", lineno=1, col_offset=2, op_type=ast.Add)


####################################################################################################
# TRANSFORMERS: BOOLOP FIXTURES
# This is a special case which has a tmp file with tests as a Python package to run the full pytest
# suite. These tests are marked with the 'slow' marker for easy filtering.
####################################################################################################


@pytest.fixture(scope="session")
def boolop_file(tmp_path_factory):
    """A simple python file with bool op operations."""
    contents = dedent(
        """\
    def equal_test(a, b):
        return a and b

    print(equal_test(1,1))
    """
    )

    fn = tmp_path_factory.mktemp("boolop") / "boolop.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def boolop_expected_loc():
    """Expected location index of the boolop fixture"""
    # Py 3.7 vs 3.8
    end_lineno = None if sys.version_info < (3, 8) else 2
    end_col_offset = None if sys.version_info < (3, 8) else 18
    return LocIndex(
        ast_class="BoolOp",
        lineno=2,
        col_offset=11,
        op_type=ast.And,
        end_lineno=end_lineno,
        end_col_offset=end_col_offset,
    )


@pytest.fixture(scope="session")
def single_binop_file_with_good_test(tmp_path_factory):
    """Single binop file and test file where mutants will be detected."""
    contents = dedent(
        """\
    def add_five(b):
        return b + 5

    print(add_five(5))
    """
    )

    test_good = dedent(
        """\
    from single import add_five

    def test_add_five():
        assert add_five(5) == 10
    """
    )

    folder = tmp_path_factory.mktemp("single_binops_good")
    fn = folder / "single.py"
    good_test_fn = folder / "test_good_single.py"

    for f, c in [(fn, contents), (good_test_fn, test_good)]:
        with open(f, "w") as output_fn:
            output_fn.write(c)

    yield FileAndTest(fn, good_test_fn)

    fn.unlink()
    good_test_fn.unlink()


@pytest.fixture(scope="session")
def single_binop_file_with_bad_test(tmp_path_factory):
    """Single binop file and test file where mutants will survive."""
    contents = dedent(
        """\
    def add_five(b):
        return b + 5

    print(add_five(5))
    """
    )

    test_bad = dedent(
        """\
    from single import add_five

    def test_add_five():
        assert True
    """
    )

    folder = tmp_path_factory.mktemp("single_binops_bad")
    fn = folder / "single.py"
    bad_test_fn = folder / "test_single_bad.py"

    for f, c in [(fn, contents), (bad_test_fn, test_bad)]:
        with open(f, "w") as output_fn:
            output_fn.write(c)

    yield FileAndTest(fn, bad_test_fn)

    fn.unlink()
    bad_test_fn.unlink()


@pytest.fixture(scope="session")
def sleep_timeout(tmp_path_factory):
    """A block of code with sleep timeout."""
    contents = dedent(
        """\
    import time
    def odd_loop(x):
        a = True
        b = False
        if a:
            time.sleep(3)
        return a

    print(odd_loop(5))
    """
    )

    test_timeout = dedent(
        """\
    from timeout import odd_loop

    def test_odd_loop():
        assert True
    """
    )

    folder = tmp_path_factory.mktemp("timeout_while")
    fn = folder / "timeout.py"
    bad_test_fn = folder / "test_timeout.py"

    for f, c in [(fn, contents), (bad_test_fn, test_timeout)]:
        with open(f, "w") as output_fn:
            output_fn.write(c)

    yield FileAndTest(fn, bad_test_fn)

    # fn.unlink()
    # bad_test_fn.unlink()


####################################################################################################
# TRANSFORMERS: COMPARE FIXTURES
####################################################################################################


@pytest.fixture(scope="session")
def compare_file(tmp_path_factory):
    """A simple python file with the compare."""
    contents = dedent(
        """\
    def equal_test(a, b):
        return a == b

    def is_test(a, b):
        return a is b

    def in_test(a, b):
        return a in b
    print(equal_test(1,1))
    """
    )

    fn = tmp_path_factory.mktemp("compare") / "compare.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def compare_expected_locs():
    """The compare expected locations based on the fixture"""
    # Py 3.7
    if sys.version_info < (3, 8):
        return [
            LocIndex(ast_class="Compare", lineno=2, col_offset=11, op_type=ast.Eq),
            LocIndex(ast_class="CompareIs", lineno=5, col_offset=11, op_type=ast.Is),
            LocIndex(ast_class="CompareIn", lineno=8, col_offset=11, op_type=ast.In),
        ]
    # Py 3.8
    return [
        LocIndex(
            ast_class="Compare",
            lineno=2,
            col_offset=11,
            op_type=ast.Eq,
            end_lineno=2,
            end_col_offset=17,
        ),
        LocIndex(
            ast_class="CompareIs",
            lineno=5,
            col_offset=11,
            op_type=ast.Is,
            end_lineno=5,
            end_col_offset=17,
        ),
        LocIndex(
            ast_class="CompareIn",
            lineno=8,
            col_offset=11,
            op_type=ast.In,
            end_lineno=8,
            end_col_offset=17,
        ),
    ]


####################################################################################################
# TRANSFORMERS: IF FIXTURES
####################################################################################################


@pytest.fixture(scope="session")
def if_file(tmp_path_factory):
    """Simple file for if-statement mutations."""
    contents = dedent(
        """\
    def equal_test(a, b):
        if a == b:
            print("Equal")
        elif a < b:
            print("LT")
        else:
            print("Else!")

    def second():
        if True:
            print("true")

        if False:
            print("false")
    """
    )

    fn = tmp_path_factory.mktemp("if_statement") / "if_statement.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def if_expected_locs():
    """Expected locations in the if_statement."""
    # Py 3.7
    if sys.version_info < (3, 8):
        return [
            LocIndex(ast_class="If", lineno=2, col_offset=4, op_type="If_Statement"),
            LocIndex(ast_class="If", lineno=4, col_offset=9, op_type="If_Statement"),
            LocIndex(ast_class="If", lineno=10, col_offset=4, op_type="If_True"),
            LocIndex(ast_class="If", lineno=13, col_offset=4, op_type="If_False"),
        ]

    # Py 3.8
    return [
        LocIndex(
            ast_class="If",
            lineno=2,
            col_offset=4,
            op_type="If_Statement",
            end_lineno=7,
            end_col_offset=22,
        ),
        LocIndex(
            ast_class="If",
            lineno=4,
            col_offset=9,
            op_type="If_Statement",
            end_lineno=7,
            end_col_offset=22,
        ),
        LocIndex(
            ast_class="If",
            lineno=10,
            col_offset=4,
            op_type="If_True",
            end_lineno=11,
            end_col_offset=21,
        ),
        LocIndex(
            ast_class="If",
            lineno=13,
            col_offset=4,
            op_type="If_False",
            end_lineno=14,
            end_col_offset=22,
        ),
    ]


####################################################################################################
# TRANSFORMERS: INDEX FIXTURES
####################################################################################################


@pytest.fixture(scope="session")
def index_file(tmp_path_factory):
    """A simple python file with the index attributes for list slices."""
    contents = dedent(
        """\
    def my_func(x_list):
        a_list = x_list[-1]
        b_list = x_list[0]
        c_list = x_list[1][2]
    """
    )

    fn = tmp_path_factory.mktemp("index") / "index.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def index_expected_locs():
    """The index expected location based on the fixture"""
    # Python 3.7
    if sys.version_info < (3, 8):
        return [
            LocIndex(ast_class="Index", lineno=2, col_offset=20, op_type="Index_NumNeg"),
            LocIndex(ast_class="Index", lineno=3, col_offset=20, op_type="Index_NumZero"),
            LocIndex(ast_class="Index", lineno=4, col_offset=20, op_type="Index_NumPos"),
            LocIndex(ast_class="Index", lineno=4, col_offset=23, op_type="Index_NumPos"),
        ]

    # Python 3.8
    return [
        LocIndex(
            ast_class="Index",
            lineno=2,
            col_offset=20,
            op_type="Index_NumNeg",
            end_lineno=2,
            end_col_offset=22,
        ),
        LocIndex(
            ast_class="Index",
            lineno=3,
            col_offset=20,
            op_type="Index_NumZero",
            end_lineno=3,
            end_col_offset=21,
        ),
        LocIndex(
            ast_class="Index",
            lineno=4,
            col_offset=20,
            op_type="Index_NumPos",
            end_lineno=4,
            end_col_offset=21,
        ),
        LocIndex(
            ast_class="Index",
            lineno=4,
            col_offset=23,
            op_type="Index_NumPos",
            end_lineno=4,
            end_col_offset=24,
        ),
    ]


####################################################################################################
# TRANSFORMERS: NAMECONST FIXTURES
####################################################################################################


@pytest.fixture(scope="session")
def nameconst_file(tmp_path_factory):
    """A simple python file with the nameconst attributes."""
    contents = dedent(
        """\
    MY_CONSTANT = True
    OTHER_CONST = {"a":1}

    def myfunc(value: bool = False):
        if bool:
            MY_CONSTANT = False
            OTHER_CONST = None
    """
    )

    fn = tmp_path_factory.mktemp("nameconst") / "nameconst.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def nameconst_expected_locs():
    """The nameconst expected location based on the fixture"""
    # Python 3.7
    if sys.version_info < (3, 8):
        return [
            LocIndex(ast_class="NameConstant", lineno=1, col_offset=14, op_type=True),
            LocIndex(ast_class="NameConstant", lineno=4, col_offset=25, op_type=False),
            LocIndex(ast_class="NameConstant", lineno=6, col_offset=22, op_type=False),
            LocIndex(ast_class="NameConstant", lineno=7, col_offset=22, op_type=None),
        ]

    # Python 3.8
    return [
        LocIndex(
            ast_class="NameConstant",
            lineno=1,
            col_offset=14,
            op_type=True,
            end_lineno=1,
            end_col_offset=18,
        ),
        LocIndex(
            ast_class="NameConstant",
            lineno=4,
            col_offset=25,
            op_type=False,
            end_lineno=4,
            end_col_offset=30,
        ),
        LocIndex(
            ast_class="NameConstant",
            lineno=6,
            col_offset=22,
            op_type=False,
            end_lineno=6,
            end_col_offset=27,
        ),
        LocIndex(
            ast_class="NameConstant",
            lineno=7,
            col_offset=22,
            op_type=None,
            end_lineno=7,
            end_col_offset=26,
        ),
    ]


####################################################################################################
# TRANSFORMERS: SLICE FIXTURES
####################################################################################################


@pytest.fixture(scope="session")
def slice_file(tmp_path_factory):
    """A simple python file with the slice attributes."""
    contents = dedent(
        """\
    def my_func(x_list):
        y_list = x_list[:-1]
        z_list = x_list[0:2:-4]
        zz_list = x_list[0::2]
        zzs_list = x_list[-8:-3:2]
        yz_list = y_list[0:]
        a_list = x_list[::]

        return yz_list
    """
    )

    fn = tmp_path_factory.mktemp("slice") / "slice.py"

    with open(fn, "w") as output_fn:
        output_fn.write(contents)

    yield fn

    fn.unlink()


@pytest.fixture(scope="session")
def slice_expected_locs():
    """The slice expected locations based on the fixture."""
    return [
        LocIndex(ast_class="Slice_Swap", lineno=2, col_offset=13, op_type="Slice_UnboundLower"),
        LocIndex(ast_class="Slice_Swap", lineno=4, col_offset=14, op_type="Slice_UnboundUpper"),
        LocIndex(ast_class="Slice_Swap", lineno=6, col_offset=14, op_type="Slice_UnboundUpper"),
    ]