# -*- coding: utf-8 -*-
# Copyright (C) 2019-2020  Nexedi SA and Contributors.
#                          Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.

from __future__ import print_function, absolute_import

import sys, os, platform, re, golang
from golang.golang_test import pyout, _pyrun
from subprocess import PIPE
from six import PY2
from six.moves import builtins
import pytest

from os.path import join, dirname, realpath
here     = dirname(__file__)
testdata = join(here, 'testdata')
testprog = join(here, 'testprog')

is_pypy    = (platform.python_implementation() == 'PyPy')
is_cpython = (platform.python_implementation() == 'CPython')

# @gpython_only is marker to run a test only under gpython
gpython_only = pytest.mark.skipif('GPython' not in sys.version, reason="gpython-only test")

# runtime is pytest fixture that yields all variants of should be supported gpython runtimes:
# '' - not specified (gpython should autoselect)
# 'gevent'
# 'threads'
@pytest.fixture(scope="function", params=['', 'gevent', 'threads'])
def runtime(request):
    yield request.param

# gpyenv returns environment appropriate for spawning gpython with
# specified runtime.
def gpyenv(runtime): # -> env
    env = os.environ.copy()
    if runtime != '':
        env['GPYTHON_RUNTIME'] = runtime
    else:
        env.pop('GPYTHON_RUNTIME', None)
    return env


@gpython_only
def test_defaultencoding_utf8():
    assert sys.getdefaultencoding() == 'utf-8'

@gpython_only
def test_golang_builtins():
    # some direct accesses
    assert go     is golang.go
    assert chan   is golang.chan
    assert select is golang.select
    assert error  is golang.error
    assert b      is golang.b
    assert u      is golang.u

    # indirectly verify golang.__all__
    for k in golang.__all__:
        assert getattr(builtins, k) is getattr(golang, k)

@gpython_only
def test_gevent_activated():
    # gpython, by default, acticates gevent.
    # handling of various runtime modes is explicitly tested in test_Xruntime.
    assert_gevent_activated()

def assert_gevent_activated():
    assert 'gevent' in sys.modules
    from gevent.monkey import is_module_patched as patched, is_object_patched as obj_patched

    # builtin (gevent: only on py2 - on py3 __import__ uses fine-grained locking)
    if PY2:
        assert obj_patched('__builtin__', '__import__')

    assert patched('socket')
    # patch_socket(dns=True) also patches vvv
    assert obj_patched('socket', 'getaddrinfo')
    assert obj_patched('socket', 'gethostbyname')
    # ...

    assert patched('time')

    assert patched('select')
    import select as select_mod # patch_select(aggressive=True) removes vvv
    assert not hasattr(select_mod, 'epoll')
    assert not hasattr(select_mod, 'kqueue')
    assert not hasattr(select_mod, 'kevent')
    assert not hasattr(select_mod, 'devpoll')

    # XXX on native windows, patch_{os,signal} do nothing currently
    if os.name != 'nt':
        assert patched('os')
        assert patched('signal')

    assert patched('thread' if PY2 else '_thread')
    assert patched('threading')
    assert patched('_threading_local')

    assert patched('ssl')
    assert patched('subprocess')
    #assert patched('sys')       # currently disabled

    if sys.hexversion >= 0x03070000: # >= 3.7.0
        assert patched('queue')

def assert_gevent_not_activated():
    assert 'gevent' not in sys.modules
    from gevent.monkey import is_module_patched as patched, is_object_patched as obj_patched

    assert not patched('socket')
    assert not patched('time')
    assert not patched('select')
    assert not patched('os')
    assert not patched('signal')
    assert not patched('thread' if PY2 else '_thread')
    assert not patched('threading')
    assert not patched('_threading_local')
    assert not patched('ssl')
    assert not patched('subprocess')
    assert not patched('sys')


@gpython_only
def test_executable(runtime):
    # sys.executable must point to gpython and we must be able to execute it.
    import gevent
    assert 'gpython' in sys.executable
    ver = pyout(['-c', 'import sys; print(sys.version)'], env=gpyenv(runtime))
    ver = str(ver)
    assert ('[GPython %s]' % golang.__version__) in ver
    if runtime != 'threads':
        assert ('[gevent %s]'  % gevent.__version__)     in ver
        assert ('[threads]')                         not in ver
    else:
        assert ('[gevent ')                          not in ver
        assert ('[threads]')                             in ver


# verify pymain.
#
# !gpython_only to make sure we get the same output when run via pymain (under
# gpython) and plain python (!gpython).
def test_pymain():
    from golang import b

    # interactive
    _ = pyout([], stdin=b'import hello\n', cwd=testdata)
    assert _ == b"hello\nworld\n['']\n"

    # -c <command>
    _ = pyout(['-c', 'import hello', 'abc', 'def'], cwd=testdata)
    assert _ == b"hello\nworld\n['-c', 'abc', 'def']\n"
    # -c<command> should also work
    __ = pyout(['-cimport hello', 'abc', 'def'], cwd=testdata)
    assert __ == _

    # -m <module>
    _ = pyout(['-m', 'hello', 'abc', 'def'], cwd=testdata)
    # realpath rewrites e.g. `local/lib -> lib` if local/lib is symlink
    hellopy = realpath(join(testdata, 'hello.py'))
    assert _ == b"hello\nworld\n['%s', 'abc', 'def']\n" % b(hellopy)
    # -m<module>
    __ = pyout(['-mhello', 'abc', 'def'], cwd=testdata)
    assert __ == _

    # file
    _ = pyout(['testdata/hello.py', 'abc', 'def'], cwd=here)
    assert _ == b"hello\nworld\n['testdata/hello.py', 'abc', 'def']\n"

    # -W <opt>
    _ = pyout(['-Werror', '-Whello', '-W', 'ignore::DeprecationWarning',
               'testprog/print_warnings_setup.py'], cwd=here)
    if PY2:
        # py2 threading, which is imported after gpython startup, adds ignore
        # for sys.exc_clear
        _ = grepv(r'ignore:sys.exc_clear:DeprecationWarning:threading:*', _)
    assert _.startswith(
        b"sys.warnoptions: ['error', 'hello', 'ignore::DeprecationWarning']\n\n" + \
        b"warnings.filters:\n" + \
        b"- ignore::DeprecationWarning::*\n" + \
        b"- error::Warning::*\n"), _
    # $PYTHONWARNINGS
    _ = pyout(['testprog/print_warnings_setup.py'], cwd=here,
              envadj={'PYTHONWARNINGS': 'ignore,world,error::SyntaxWarning'})
    if PY2:
        # see ^^^
        _ = grepv(r'ignore:sys.exc_clear:DeprecationWarning:threading:*', _)
    assert _.startswith(
        b"sys.warnoptions: ['ignore', 'world', 'error::SyntaxWarning']\n\n" + \
        b"warnings.filters:\n" + \
        b"- error::SyntaxWarning::*\n" + \
        b"- ignore::Warning::*\n"), _

# verify that pymain sets sys.path in exactly the same way as underlying python does.
@gpython_only
def test_pymain_syspath():
    # check verifies that print_syspath output for gpython and underlying python is the same.
    # if path0cwd2realpath=Y, expect realpath('') instead of '' in sys.path[0]
    def check(argv, path0cwd2realpath=False, **kw):
        gpyout   = u(pyout(argv, **kw))
        stdpyout = u(pyout(argv, pyexe=sys._gpy_underlying_executable, **kw))
        gpyoutv   = gpyout.splitlines()
        stdpyoutv = stdpyout.splitlines()
        if path0cwd2realpath:
            assert stdpyoutv[0] == ''
            stdpyoutv[0] = realpath(kw.get('cwd', ''))

        assert gpyoutv == stdpyoutv

    check([], stdin=b'import print_syspath', cwd=testprog)  # interactive
    check(['-c', 'import print_syspath'], cwd=testprog)     # -c
    check(['-m', 'print_syspath'], cwd=testprog,            # -m
            path0cwd2realpath=(PY2 or is_pypy))
    check(['testprog/print_syspath.py'], cwd=here)          # file

# pymain -V/--version
# gpython_only because output differs from !gpython.
@gpython_only
def test_pymain_ver(runtime):
    from golang import b
    from gpython import _version_info_str as V
    import gevent
    vok = 'GPython %s' % golang.__version__
    if runtime != 'threads':
        vok += ' [gevent %s]' % gevent.__version__
    else:
        vok += ' [threads]'

    if is_cpython:
        vok += ' / CPython %s' % platform.python_version()
    elif is_pypy:
        vok += ' / PyPy %s / Python %s' % (V(sys.pypy_version_info), V(sys.version_info))
    else:
        vok = sys.version

    vok += '\n'

    ret, out, err = _pyrun(['-V'], stdout=PIPE, stderr=PIPE, env=gpyenv(runtime))
    assert (ret, out, b(err)) == (0, b'', b(vok))

    ret, out, err = _pyrun(['--version'], stdout=PIPE, stderr=PIPE, env=gpyenv(runtime))
    assert (ret, out, b(err)) == (0, b'', b(vok))


# verify -X gpython.runtime=...
@gpython_only
def test_Xruntime(runtime):
    env = os.environ.copy()
    env.pop('GPYTHON_RUNTIME', None) # del

    argv = []
    if runtime != '':
        argv += ['-X', 'gpython.runtime='+runtime]
    prog = 'from gpython import gpython_test as t; '
    if runtime != 'threads':
        prog += 't.assert_gevent_activated(); '
    else:
        prog += 't.assert_gevent_not_activated(); '
    prog += 'print("ok")'
    argv += ['-c', prog]

    out = pyout(argv, env=env)
    assert out == b'ok\n'


# ---- misc ----

# grepv filters out lines matching pattern from text.
def grepv(pattern, text): # -> text
    if isinstance(text, bytes):
        t = b''
    else:
        t = ''
    p = re.compile(pattern)
    v = []
    for l in text.splitlines(True):
        m = p.search(l)
        if not m:
            v.append(l)
    return t.join(v)
