# -*- coding: utf-8 -*-
"""This file contains code for setting up C++ unit tests with gtest
from within Python tests using py.test.

"""
from __future__ import print_function

import pytest
import os
import inspect
from collections import defaultdict
import subprocess

from ffc.backends.ufc import get_include_path


# TODO: For a generic framework, this needs to change somewhat:
_supportcode = '''
#include <ufc.h>
#include <ufc_geometry.h>
#include "mock_cells.h"
//#include "debugging.h"
'''

_gtest_runner_template = """
#include <gtest/gtest.h>

{supportcode}

{testincludes}

int main(int argc, char **argv)
{{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}}
"""

_gtest_template = """
/** Autogenerated from Python test
    {pytestname}
    at
    {pyfilename}:{pylineno}
*/
TEST ({suite}, {case})
{{
{body}
}}
"""


def find_parent_test_function():
    """Return (filename, lineno, functionname) of the
    first function named "test_*" found on the stack."""

    # Get current frame
    frame = inspect.currentframe()
    info = inspect.getframeinfo(frame)

    # Jump back frames until we're in a 'test_*' function
    while not info[2].startswith("test_"):
        frame = frame.f_back
        info = inspect.getframeinfo(frame)

    # Get info from frame
    filename = info[0]
    lineno = info[1]
    function = info[2]

    assert len(info) == 5
    assert function.startswith("test_")

    return filename, lineno, function


class GTestContext:
    _all = []

    def __init__(self, config):
        self._basedir = os.path.dirname(__file__)
        self._gendir = os.path.join(self._basedir, "generated")
        self._binary_filename = os.path.join(self._basedir, "run_gtest")
        self._gtest_log = os.path.join(self._basedir, "gtest.log")
        self._code = defaultdict(list)
        self._dirlist = []
        GTestContext._all.append(self)


    def info(self, msg):
        pre = "In gtest generation:"
        if '\n' in msg:
            print(pre)
            print(msg)
        else:
            print(pre, msg)


    def add(self, body):
        # Look through stack to find frame with test_... function name
        filename, lineno, function = find_parent_test_function()

        # Using function testcase class name as suite and function
        # name as case
        basename = os.path.basename(filename)
        suite = basename.replace(".py", "")
        case = function

        # Use a header filename matching the python filename
        hfilename = os.path.join(self._gendir, basename.replace(".py", ".h"))

        # Get python filename relative to test directory
        pyfilename = os.path.relpath(filename, self._basedir)

        # Format as a test in target framework and store
        code = _gtest_template.format(pyfilename=pyfilename,
                                      pylineno=lineno,
                                      pytestname=function,
                                      suite=suite,
                                      case=case,
                                      body=body)
        self._code[hfilename].append(code)


    def write(self):
        # Make sure we have the directory for generated code
        if not os.path.isdir(self._gendir):
            os.mkdir(self._gendir)

        # Write test code collected during py.test run to files
        headers = []
        header_basenames = []
        for hfilename in sorted(self._code):
            tests = self._code[hfilename]
            code = '\n'.join(tests)
            with open(hfilename, "w") as f:
                f.write(code)
                headers.append(hfilename)

        # Collect headers in include list for runner code
        self._test_header_names = [os.path.split(h)[-1] for h in headers]
        testincludes = '\n'.join('#include "{0}"'.format(h) for h in self._test_header_names)

        # Write test runner code to file
        runner_code = _gtest_runner_template.format(supportcode=_supportcode,
                                                    testincludes=testincludes)

        self._main_filename = os.path.join(self._gendir, "main.cpp")
        with open(self._main_filename, "w") as f:
            f.write(runner_code)


    def build_gtest(self):
        "Build gtest library"

        # Source and build directories
        gtest_dir = "../../../libs/gtest-1.7.0"
        gtest_dir = os.path.abspath(os.path.join(self._basedir, gtest_dir))
        build_dir = os.path.join(gtest_dir, "lib")

        # Check if GTest source can be found
        if os.path.isdir(gtest_dir):
            # Make build directory, if required
            if not os.path.isdir(build_dir):
                os.mkdir(build_dir)

            # Configure gtest using cmake
            error = subprocess.call("cmake ..", cwd=build_dir, shell=True)
            if error:
                raise RuntimeError("Could not call CMake successfully to build gtest")

            # Build gtest library
            err = subprocess.call("make", cwd=build_dir, shell=True)
            if error:
                raise RuntimeError("Could not call make successfully to build gtest")

        else:
            raise RuntimeError("Cannot find gtest source")


    def build(self):
        # Prepare command
        UFC_INCLUDE_DIR = get_include_path()
        if not os.path.exists(os.path.join(UFC_INCLUDE_DIR, "ufc.h")):
            import IPython; IPython.embed()
            raise RuntimeError("Cannot find ufc.h in provided include path: %s" % (UFC_INCLUDE_DIR,))
        UFC_CXX_FLAGS = " ".join(["-g", "-O0", "-std=c++11", "-Wall"])
        cmd = ['make', 'UFC_INCLUDE_DIR="%s"' % UFC_INCLUDE_DIR, 'CXXFLAGS="%s"' % UFC_CXX_FLAGS]
        self.info("Running command: " + " ".join(cmd))

        # Execute
        try:
            out = subprocess.check_output(cmd,
                                          cwd=self._basedir, shell=True,
                                          universal_newlines=True)
            self.info(out)
            self.info("Building ok.")
        except subprocess.CalledProcessError as e:
            self.info("Building '{0}' FAILED (code {1}, headers: {2})".format(self._binary_filename,
                                                                              e.returncode, self._test_header_names))
            self.info("Build output:")
            self.info(e.output)
            pytest.fail()


    def run(self):
        try:
            out = subprocess.check_output(self._binary_filename,
                                          cwd=self._basedir, shell=True,
                                          universal_newlines=True)
            self.info("Gtest running ok!")
            with open(self._gtest_log, "w") as f:
                f.write(out)
            self.info(out)
        except subprocess.CalledProcessError as e:
            self.info("Gtest running FAILED with code {0}!".format(e.returncode))
            with open(self._gtest_log, "w") as f:
                f.write(e.output)
            self.info(e.output)
            pytest.fail()


    def finalize(self):
        # Build gtest library itself
        self.build_gtest()

        # Write collected test code to files
        self.write()

        # Build test code
        self.build()

        # Run compiled tests
        self.run()


@pytest.fixture("session")
def gtest():
    "create initial files for gtest generation"
    config = None
    gtc = GTestContext(config)
    return gtc


def gtest_sessionfinish(session):
    session.trace("finalizing gtest contexts")
    while GTestContext._all:
        gtc = GTestContext._all.pop()
        gtc.finalize()
    session.trace("done finalizing gtest contexts")


def pytest_sessionfinish(session):
    gtest_sessionfinish(session)
