from __future__ import absolute_import, division, print_function
from textwrap import dedent

import _pytest._code
import py
import pytest
from _pytest.config import PytestPluginManager
from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR


@pytest.fixture(scope="module", params=["global", "inpackage"])
def basedir(request, tmpdir_factory):
    from _pytest.tmpdir import tmpdir
    tmpdir = tmpdir(request, tmpdir_factory)
    tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3")
    tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5")
    if request.param == "inpackage":
        tmpdir.ensure("adir/__init__.py")
        tmpdir.ensure("adir/b/__init__.py")
    return tmpdir


def ConftestWithSetinitial(path):
    conftest = PytestPluginManager()
    conftest_setinitial(conftest, [path])
    return conftest


def conftest_setinitial(conftest, args, confcutdir=None):
    class Namespace(object):
        def __init__(self):
            self.file_or_dir = args
            self.confcutdir = str(confcutdir)
            self.noconftest = False
    conftest._set_initial_conftests(Namespace())


class TestConftestValueAccessGlobal(object):
    def test_basic_init(self, basedir):
        conftest = PytestPluginManager()
        p = basedir.join("adir")
        assert conftest._rget_with_confmod("a", p)[1] == 1

    def test_immediate_initialiation_and_incremental_are_the_same(self, basedir):
        conftest = PytestPluginManager()
        len(conftest._path2confmods)
        conftest._getconftestmodules(basedir)
        snap1 = len(conftest._path2confmods)
        # assert len(conftest._path2confmods) == snap1 + 1
        conftest._getconftestmodules(basedir.join('adir'))
        assert len(conftest._path2confmods) == snap1 + 1
        conftest._getconftestmodules(basedir.join('b'))
        assert len(conftest._path2confmods) == snap1 + 2

    def test_value_access_not_existing(self, basedir):
        conftest = ConftestWithSetinitial(basedir)
        with pytest.raises(KeyError):
            conftest._rget_with_confmod('a', basedir)

    def test_value_access_by_path(self, basedir):
        conftest = ConftestWithSetinitial(basedir)
        adir = basedir.join("adir")
        assert conftest._rget_with_confmod("a", adir)[1] == 1
        assert conftest._rget_with_confmod("a", adir.join("b"))[1] == 1.5

    def test_value_access_with_confmod(self, basedir):
        startdir = basedir.join("adir", "b")
        startdir.ensure("xx", dir=True)
        conftest = ConftestWithSetinitial(startdir)
        mod, value = conftest._rget_with_confmod("a", startdir)
        assert value == 1.5
        path = py.path.local(mod.__file__)
        assert path.dirpath() == basedir.join("adir", "b")
        assert path.purebasename.startswith("conftest")


def test_conftest_in_nonpkg_with_init(tmpdir):
    tmpdir.ensure("adir-1.0/conftest.py").write("a=1 ; Directory = 3")
    tmpdir.ensure("adir-1.0/b/conftest.py").write("b=2 ; a = 1.5")
    tmpdir.ensure("adir-1.0/b/__init__.py")
    tmpdir.ensure("adir-1.0/__init__.py")
    ConftestWithSetinitial(tmpdir.join("adir-1.0", "b"))


def test_doubledash_considered(testdir):
    conf = testdir.mkdir("--option")
    conf.ensure("conftest.py")
    conftest = PytestPluginManager()
    conftest_setinitial(conftest, [conf.basename, conf.basename])
    values = conftest._getconftestmodules(conf)
    assert len(values) == 1


def test_issue151_load_all_conftests(testdir):
    names = "code proj src".split()
    for name in names:
        p = testdir.mkdir(name)
        p.ensure("conftest.py")

    conftest = PytestPluginManager()
    conftest_setinitial(conftest, names)
    d = list(conftest._conftestpath2mod.values())
    assert len(d) == len(names)


def test_conftest_global_import(testdir):
    testdir.makeconftest("x=3")
    p = testdir.makepyfile("""
        import py, pytest
        from _pytest.config import PytestPluginManager
        conf = PytestPluginManager()
        mod = conf._importconftest(py.path.local("conftest.py"))
        assert mod.x == 3
        import conftest
        assert conftest is mod, (conftest, mod)
        subconf = py.path.local().ensure("sub", "conftest.py")
        subconf.write("y=4")
        mod2 = conf._importconftest(subconf)
        assert mod != mod2
        assert mod2.y == 4
        import conftest
        assert conftest is mod2, (conftest, mod)
    """)
    res = testdir.runpython(p)
    assert res.ret == 0


def test_conftestcutdir(testdir):
    conf = testdir.makeconftest("")
    p = testdir.mkdir("x")
    conftest = PytestPluginManager()
    conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p)
    values = conftest._getconftestmodules(p)
    assert len(values) == 0
    values = conftest._getconftestmodules(conf.dirpath())
    assert len(values) == 0
    assert conf not in conftest._conftestpath2mod
    # but we can still import a conftest directly
    conftest._importconftest(conf)
    values = conftest._getconftestmodules(conf.dirpath())
    assert values[0].__file__.startswith(str(conf))
    # and all sub paths get updated properly
    values = conftest._getconftestmodules(p)
    assert len(values) == 1
    assert values[0].__file__.startswith(str(conf))


def test_conftestcutdir_inplace_considered(testdir):
    conf = testdir.makeconftest("")
    conftest = PytestPluginManager()
    conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath())
    values = conftest._getconftestmodules(conf.dirpath())
    assert len(values) == 1
    assert values[0].__file__.startswith(str(conf))


@pytest.mark.parametrize("name", 'test tests whatever .dotdir'.split())
def test_setinitial_conftest_subdirs(testdir, name):
    sub = testdir.mkdir(name)
    subconftest = sub.ensure("conftest.py")
    conftest = PytestPluginManager()
    conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir)
    if name not in ('whatever', '.dotdir'):
        assert subconftest in conftest._conftestpath2mod
        assert len(conftest._conftestpath2mod) == 1
    else:
        assert subconftest not in conftest._conftestpath2mod
        assert len(conftest._conftestpath2mod) == 0


def test_conftest_confcutdir(testdir):
    testdir.makeconftest("assert 0")
    x = testdir.mkdir("x")
    x.join("conftest.py").write(_pytest._code.Source("""
        def pytest_addoption(parser):
            parser.addoption("--xyz", action="store_true")
    """))
    result = testdir.runpytest("-h", "--confcutdir=%s" % x, x)
    result.stdout.fnmatch_lines(["*--xyz*"])
    assert 'warning: could not load initial' not in result.stdout.str()


def test_no_conftest(testdir):
    testdir.makeconftest("assert 0")
    result = testdir.runpytest("--noconftest")
    assert result.ret == EXIT_NOTESTSCOLLECTED

    result = testdir.runpytest()
    assert result.ret == EXIT_USAGEERROR


def test_conftest_existing_resultlog(testdir):
    x = testdir.mkdir("tests")
    x.join("conftest.py").write(_pytest._code.Source("""
        def pytest_addoption(parser):
            parser.addoption("--xyz", action="store_true")
    """))
    testdir.makefile(ext=".log", result="")  # Writes result.log
    result = testdir.runpytest("-h", "--resultlog", "result.log")
    result.stdout.fnmatch_lines(["*--xyz*"])


def test_conftest_existing_junitxml(testdir):
    x = testdir.mkdir("tests")
    x.join("conftest.py").write(_pytest._code.Source("""
        def pytest_addoption(parser):
            parser.addoption("--xyz", action="store_true")
    """))
    testdir.makefile(ext=".xml", junit="")  # Writes junit.xml
    result = testdir.runpytest("-h", "--junitxml", "junit.xml")
    result.stdout.fnmatch_lines(["*--xyz*"])


def test_conftest_import_order(testdir, monkeypatch):
    ct1 = testdir.makeconftest("")
    sub = testdir.mkdir("sub")
    ct2 = sub.join("conftest.py")
    ct2.write("")

    def impct(p):
        return p

    conftest = PytestPluginManager()
    conftest._confcutdir = testdir.tmpdir
    monkeypatch.setattr(conftest, '_importconftest', impct)
    assert conftest._getconftestmodules(sub) == [ct1, ct2]


def test_fixture_dependency(testdir, monkeypatch):
    ct1 = testdir.makeconftest("")
    ct1 = testdir.makepyfile("__init__.py")
    ct1.write("")
    sub = testdir.mkdir("sub")
    sub.join("__init__.py").write("")
    sub.join("conftest.py").write(dedent("""
        import pytest

        @pytest.fixture
        def not_needed():
            assert False, "Should not be called!"

        @pytest.fixture
        def foo():
            assert False, "Should not be called!"

        @pytest.fixture
        def bar(foo):
            return 'bar'
    """))
    subsub = sub.mkdir("subsub")
    subsub.join("__init__.py").write("")
    subsub.join("test_bar.py").write(dedent("""
        import pytest

        @pytest.fixture
        def bar():
            return 'sub bar'

        def test_event_fixture(bar):
            assert bar == 'sub bar'
    """))
    result = testdir.runpytest("sub")
    result.stdout.fnmatch_lines(["*1 passed*"])


def test_conftest_found_with_double_dash(testdir):
    sub = testdir.mkdir("sub")
    sub.join("conftest.py").write(dedent("""
        def pytest_addoption(parser):
            parser.addoption("--hello-world", action="store_true")
    """))
    p = sub.join("test_hello.py")
    p.write("def test_hello(): pass")
    result = testdir.runpytest(str(p) + "::test_hello", "-h")
    result.stdout.fnmatch_lines("""
        *--hello-world*
    """)


class TestConftestVisibility(object):
    def _setup_tree(self, testdir):  # for issue616
        # example mostly taken from:
        # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html
        runner = testdir.mkdir("empty")
        package = testdir.mkdir("package")

        package.join("conftest.py").write(dedent("""\
            import pytest
            @pytest.fixture
            def fxtr():
                return "from-package"
        """))
        package.join("test_pkgroot.py").write(dedent("""\
            def test_pkgroot(fxtr):
                assert fxtr == "from-package"
        """))

        swc = package.mkdir("swc")
        swc.join("__init__.py").ensure()
        swc.join("conftest.py").write(dedent("""\
            import pytest
            @pytest.fixture
            def fxtr():
                return "from-swc"
        """))
        swc.join("test_with_conftest.py").write(dedent("""\
            def test_with_conftest(fxtr):
                assert fxtr == "from-swc"

        """))

        snc = package.mkdir("snc")
        snc.join("__init__.py").ensure()
        snc.join("test_no_conftest.py").write(dedent("""\
            def test_no_conftest(fxtr):
                assert fxtr == "from-package"   # No local conftest.py, so should
                                                # use value from parent dir's

        """))
        print("created directory structure:")
        for x in testdir.tmpdir.visit():
            print("   " + x.relto(testdir.tmpdir))

        return {
            "runner": runner,
            "package": package,
            "swc": swc,
            "snc": snc}

    # N.B.: "swc" stands for "subdir with conftest.py"
    #       "snc" stands for "subdir no [i.e. without] conftest.py"
    @pytest.mark.parametrize("chdir,testarg,expect_ntests_passed", [
        # Effective target: package/..
        ("runner", "..", 3),
        ("package", "..", 3),
        ("swc", "../..", 3),
        ("snc", "../..", 3),

        # Effective target: package
        ("runner", "../package", 3),
        ("package", ".", 3),
        ("swc", "..", 3),
        ("snc", "..", 3),

        # Effective target: package/swc
        ("runner", "../package/swc", 1),
        ("package", "./swc", 1),
        ("swc", ".", 1),
        ("snc", "../swc", 1),

        # Effective target: package/snc
        ("runner", "../package/snc", 1),
        ("package", "./snc", 1),
        ("swc", "../snc", 1),
        ("snc", ".", 1),
    ])
    @pytest.mark.issue616
    def test_parsefactories_relative_node_ids(
            self, testdir, chdir, testarg, expect_ntests_passed):
        dirs = self._setup_tree(testdir)
        print("pytest run in cwd: %s" % (
              dirs[chdir].relto(testdir.tmpdir)))
        print("pytestarg        : %s" % (testarg))
        print("expected pass    : %s" % (expect_ntests_passed))
        with dirs[chdir].as_cwd():
            reprec = testdir.inline_run(testarg, "-q", "--traceconfig")
            reprec.assertoutcome(passed=expect_ntests_passed)


@pytest.mark.parametrize('confcutdir,passed,error', [
    ('.', 2, 0),
    ('src', 1, 1),
    (None, 1, 1),
])
def test_search_conftest_up_to_inifile(testdir, confcutdir, passed, error):
    """Test that conftest files are detected only up to a ini file, unless
    an explicit --confcutdir option is given.
    """
    root = testdir.tmpdir
    src = root.join('src').ensure(dir=1)
    src.join('pytest.ini').write('[pytest]')
    src.join('conftest.py').write(_pytest._code.Source("""
        import pytest
        @pytest.fixture
        def fix1(): pass
    """))
    src.join('test_foo.py').write(_pytest._code.Source("""
        def test_1(fix1):
            pass
        def test_2(out_of_reach):
            pass
    """))
    root.join('conftest.py').write(_pytest._code.Source("""
        import pytest
        @pytest.fixture
        def out_of_reach(): pass
    """))

    args = [str(src)]
    if confcutdir:
        args = ['--confcutdir=%s' % root.join(confcutdir)]
    result = testdir.runpytest(*args)
    match = ''
    if passed:
        match += '*%d passed*' % passed
    if error:
        match += '*%d error*' % error
    result.stdout.fnmatch_lines(match)


def test_issue1073_conftest_special_objects(testdir):
    testdir.makeconftest("""
        class DontTouchMe(object):
            def __getattr__(self, x):
                raise Exception('cant touch me')

        x = DontTouchMe()
    """)
    testdir.makepyfile("""
        def test_some():
            pass
    """)
    res = testdir.runpytest()
    assert res.ret == 0


def test_conftest_exception_handling(testdir):
    testdir.makeconftest('''
        raise ValueError()
    ''')
    testdir.makepyfile("""
        def test_some():
            pass
    """)
    res = testdir.runpytest()
    assert res.ret == 4
    assert 'raise ValueError()' in [line.strip() for line in res.errlines]


def test_hook_proxy(testdir):
    """Session's gethookproxy() would cache conftests incorrectly (#2016).
    It was decided to remove the cache altogether.
    """
    testdir.makepyfile(**{
        'root/demo-0/test_foo1.py': "def test1(): pass",

        'root/demo-a/test_foo2.py': "def test1(): pass",
        'root/demo-a/conftest.py': """
            def pytest_ignore_collect(path, config):
                return True
            """,

        'root/demo-b/test_foo3.py': "def test1(): pass",
        'root/demo-c/test_foo4.py': "def test1(): pass",
    })
    result = testdir.runpytest()
    result.stdout.fnmatch_lines([
        '*test_foo1.py*',
        '*test_foo3.py*',
        '*test_foo4.py*',
        '*3 passed*',
    ])


def test_required_option_help(testdir):
    testdir.makeconftest("assert 0")
    x = testdir.mkdir("x")
    x.join("conftest.py").write(_pytest._code.Source("""
        def pytest_addoption(parser):
            parser.addoption("--xyz", action="store_true", required=True)
    """))
    result = testdir.runpytest("-h", x)
    assert 'argument --xyz is required' not in result.stdout.str()
    assert 'general:' in result.stdout.str()