#
# gPrime - A web-based genealogy program
#
# Copyright (C) 2000-2007  Donald N. Allingham
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

# test/test_util.py

"""unittest support utility module"""

import os
import sys
import traceback
import tempfile
import shutil
import logging
import contextlib
from io import TextIOWrapper, BytesIO, StringIO

from gprime.dbstate import DbState
from gprime.cli.user import User
from gprime.cli.grampscli import CLIManager
from gprime.cli.argparser import ArgParser
from gprime.cli.arghandler import ArgHandler

# _caller_context is primarily here to support and document the process
# of determining the test-module's directory.
#
# NB: the traceback 0-element is 'limit'-levels back, or earliest calling
# context if that is less than limit.
#  The -1 element is this very function; -2 is its caller, etc.
# A traceback context tuple is:
#  (file, line, active function, text of the call-line)
def _caller_context():
    """Return context of first caller outside this module"""
    lim = 5  #  1 for this function, plus futrher chain within this module
    st = traceback.extract_stack(limit=lim)
    thisfile = __file__.rstrip("co")  # eg, in ".py[co]
    while st and st[-1][0] == thisfile:
        del(st[-1])
    if not st:
        raise TestError("Unexpected function call chain length!")
    return st[-1]


# NB: tb[0] differs between running 'XYZ_test.py' and './XYZ_test.py'
#     so, always take the abspath.
def _caller_dir():
    """Return directory of caller function (caller outside this module)"""
    tb =  _caller_context()
    return os.path.dirname(os.path.abspath(tb[0]))


class TestError(Exception):
    """Exception for use by test modules

    Use this, for example, to distuinguish testing errors.

    """
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)


def msg(got, exp, msg, pfx=""):
    """Error-report message formatting utility

    This improves unittest failure messages by showing data values
    Usage:
      assertEqual(got,exp, msg(got,exp,"mess" [,prefix])
    The failure message will show as
      [prefix: ] mess
        .....got:repr(value-of-got)
        expected:repr(value-of-exp)

    """
    if pfx:
        pfx += ": "
    return "%s%s\n .....got:%r\n expected:%r" % (pfx, msg, got, exp)


def absdir(path=None):
    """Return absolute dir of the specified path

    The path parm may be dir or file  or missing.
    If a file, the dir of the file is used.
    If missing, the dir of test-module caller is used

    Common usage is
      here = absdir()
      here = absdir(__file__)
    These 2 return the same result

    """
    if not path:
        path = _caller_dir()
    loc = os.path.abspath(path)
    if os.path.isfile(loc):
        loc = os.path.dirname(loc)
    return loc


def path_append_parent(path=None):
    """Append (if required) the parent of a path to the python system path,
    and return the abspath to the parent as a possible convenience

    The path parm may be a dir or a file or missing.
      If a file, the dir of the file is used.
      If missing the test-module caller's dir is used.
    And then the parent of that dir is appended (if not already present)

    Common usage is
      path_append_parent()
      path_append_parent(__file__)
    These 2 produce the same result

    """
    pdir = os.path.dirname(absdir(path))
    if not pdir in sys.path:
        sys.path.append(pdir)
    return pdir


def make_subdir(dir, parent=None):
    """Make (if required) a subdir to a given parent and return its path

    The parent parm may be dir or file or missing
      If a file, the dir of the file us used
      If missing, the test-module caller's dir is used
    Then the subdir dir in the parent dir is created if not already present

    """
    if not parent:
        parent = _caller_dir()
    sdir = os.path.join(parent,dir)
    if not os.path.exists(sdir):
        os.mkdir(sdir)
    return sdir

def delete_tree(dir):
    """Recursively delete directory and content

    WARNING: this is clearly dangerous
      it will only operate on subdirs of the test module dir or of /tmp

    Test writers may explicitly use shutil.rmtree if really needed
    """

    if not os.path.isdir(dir):
        raise TestError("%r is not a dir" % dir)
    sdir = os.path.abspath(dir)
    here = _caller_dir() + os.path.sep
    tmp = tempfile.gettempdir() + os.path.sep
    if not (sdir.startswith(here) or sdir.startswith(tmp)):
        raise TestError("%r is not a subdir of here (%r) or %r"
            % (dir, here, tmp))
    shutil.rmtree(sdir)

# simplified logging
# gramps-independent but gramps-compatible
#
# I don't see any need to inherit from logging.Logger
# (at present, test code needs nothing fancy)
# but that might be considered for future needs
# NB: current code reflects limited expertise on the
# uses of the logging module
# ---------------------------------------------------------
class TestLogger:
    """this class mainly just encapsulates some globals
    namely lfname, lfh  for a file log name and handle

    provides simplified logging setup for test modules
    that need to setup logging for modules under test
    (just instantiate a TestLogger to avoid error
     messages about logging handlers not available)

    There is also a simple logfile capability, to allow
    test modules to capture gramps logging output

    Note that existing logging will still occur, possibly
    resulting in console messages and popup dialogs
    """
    def __init__(self, lvl=logging.WARN):
        logging.basicConfig(level=lvl)

    def logfile_init(self, lfname):
        """init or re-init a logfile"""
        if getattr(self, "lfh", None):
            logging.getLogger().handlers.remove(self.lfh)
        if os.path.isfile(lfname):
            os.unlink(lfname)
        self.lfh = logging.FileHandler(lfname)
        logging.getLogger().addHandler(self.lfh)
        self.lfname = lfname

    def logfile_getlines(self):
        """get current content of logfile as list of lines"""
        txt = []
        if self.lfname and os.path.isfile(self.lfname):
            txt = open(self.lfname).readlines()
        return txt

### Support for testing CLI

def new_exit(edit_code=None):
    raise SystemExit()

@contextlib.contextmanager
def capture(stdin):
    oldout, olderr = sys.stdout, sys.stderr
    oldexit = sys.exit
    if stdin:
        oldin = sys.stdin
        sys.stdin = stdin
    try:
        output = [StringIO(), StringIO()]
        sys.stdout, sys.stderr = output
        sys.exit = new_exit
        yield output
    except SystemExit:
        pass
    finally:
        sys.stdout, sys.stderr = oldout, olderr
        sys.exit = oldexit
        if stdin:
            sys.stdin = oldin
        output[0] = output[0].getvalue()
        output[1] = output[1].getvalue()

class Gramps:
    def __init__(self, user=None, dbstate=None):
        ## Setup:
        from gprime.cli.clidbman import CLIDbManager
        self.dbstate = dbstate or DbState()
        #we need a manager for the CLI session
        self.user = user or User(auto_accept=True, quiet=False)
        self.climanager = CLIManager(self.dbstate, setloader=True, user=self.user)
        self.clidbmanager = CLIDbManager(self.dbstate)

    def run(self, *args, stdin=None):
        with capture(stdin) as output:
            #load the plugins
            self.climanager.do_reg_plugins(self.dbstate, uistate=None)
            # handle the arguments
            args = [sys.executable] + list(args)
            argparser = ArgParser(args)
            argparser.need_gui() # initializes some variables
            if argparser.errors:
                print(argparser.errors, file=sys.stderr)
            argparser.print_help()
            argparser.print_usage()
            handler = ArgHandler(self.dbstate, argparser, self.climanager)
            # create a manager to manage the database
            handler.handle_args_cli()
            if handler.dbstate.is_open():
                handler.dbstate.db.close()
        return output

#===eof===
