import gc
import logging
import os
import sys
import tempfile
import unittest
import winreg

import pythoncom
import pywin32_testutil
import pywintypes
import win32api
import win32com
import winerror
from pythoncom import _GetGatewayCount, _GetInterfaceCount
from pywin32_testutil import LeakTestCase, TestLoader, TestResult, TestRunner


def CheckClean():
    # Ensure no lingering exceptions - Python should have zero outstanding
    # COM objects
    c = _GetInterfaceCount()
    if c:
        print("Warning - %d com interface objects still alive" % c)
    c = _GetGatewayCount()
    if c:
        print("Warning - %d com gateway objects still alive" % c)


def RegisterPythonServer(filename, progids=None, verbose=0):
    if progids:
        if isinstance(progids, str):
            progids = [progids]
        # we know the CLSIDs we need, but we might not be an admin user
        # and otherwise unable to register them.  So as long as the progids
        # exist and the DLL points at our version, assume it already is.
        why_not = None
        for progid in progids:
            try:
                clsid = pywintypes.IID(progid)
            except pythoncom.com_error:
                # not registered.
                break
            try:
                HKCR = winreg.HKEY_CLASSES_ROOT
                hk = winreg.OpenKey(HKCR, "CLSID\\%s" % clsid)
                dll = winreg.QueryValue(hk, "InprocServer32")
            except OSError:
                # no CLSID or InProcServer32 - not registered
                break
            ok_files = [
                os.path.basename(pythoncom.__file__),
                "pythoncomloader%d%d.dll"
                % (sys.version_info.major, sys.version_info.minor),
            ]
            if os.path.basename(dll) not in ok_files:
                why_not = (
                    "{!r} is registered against a different Python version ({})".format(
                        progid,
                        dll,
                    )
                )
                break
        else:
            # print(f"Skipping registration of '{filename}' - already registered")
            return
    # needs registration - see if it's likely!
    try:
        from win32com.shell.shell import IsUserAnAdmin
    except ImportError:
        print("Can't import win32com.shell - no idea if you are an admin or not?")
        is_admin = False
    else:
        try:
            is_admin = IsUserAnAdmin()
        except pythoncom.com_error:
            # old, less-secure OS - assume *is* admin.
            is_admin = True
    if not is_admin:
        msg = (
            "%r isn't registered, but I'm not an administrator who can register it."
            % progids[0]
        )
        if why_not:
            msg += "\n(registration check failed as %s)" % why_not
        # throw a normal "class not registered" exception - we don't report
        # them the same way as "real" errors.
        raise pythoncom.com_error(winerror.CO_E_CLASSSTRING, msg, None, -1)
    # so theoretically we are able to register it.
    cmd = f'{win32api.GetModuleFileName(0)} "{filename}" --unattended > nul 2>&1'
    if verbose:
        print("Registering engine", filename)
        # print(cmd)
    rc = os.system(cmd)
    if rc:
        print("Registration command was:")
        print(cmd)
        raise RuntimeError("Registration of engine '%s' failed" % filename)


def ExecuteShellCommand(
    cmd,
    testcase,
    expected_output=None,  # Set to '' to check for nothing
    tracebacks_ok=0,  # OK if the output contains a t/b?
):
    output_name = tempfile.mktemp("win32com_test")
    cmd += ' > "%s" 2>&1' % output_name
    rc = os.system(cmd)
    output = open(output_name, "r").read().strip()
    os.remove(output_name)

    class Failed(Exception):
        pass

    try:
        if rc:
            raise Failed("exit code was " + str(rc))
        if expected_output is not None and output != expected_output:
            raise Failed(f"Expected output {expected_output!r} (got {output!r})")
        if not tracebacks_ok and output.find("Traceback (most recent call last)") >= 0:
            raise Failed("traceback in program output")
        return output
    except Failed as why:
        print("Failed to exec command '%r'" % cmd)
        print("Failed as", why)
        print("** start of program output **")
        print(output)
        print("** end of program output **")
        testcase.fail(f"Executing '{cmd}' failed as {why}")


def assertRaisesCOM_HRESULT(testcase, hresult, func, *args, **kw):
    try:
        func(*args, **kw)
    except pythoncom.com_error as details:
        if details.hresult == hresult:
            return
    testcase.fail("Excepected COM exception with HRESULT 0x%x" % hresult)


class CaptureWriter:
    def __init__(self):
        self.old_err = self.old_out = None
        self.clear()

    def capture(self):
        self.clear()
        self.old_out = sys.stdout
        self.old_err = sys.stderr
        sys.stdout = sys.stderr = self

    def release(self):
        if self.old_out:
            sys.stdout = self.old_out
            self.old_out = None
        if self.old_err:
            sys.stderr = self.old_err
            self.old_err = None

    def clear(self):
        self.captured = []

    def write(self, msg):
        self.captured.append(msg)

    def get_captured(self):
        return "".join(self.captured)

    def get_num_lines_captured(self):
        return len("".join(self.captured).split("\n"))


# Utilities to set the win32com logger to something what just captures
# records written and doesn't print them.
class LogHandler(logging.Handler):
    def __init__(self):
        self.emitted = []
        logging.Handler.__init__(self)

    def emit(self, record):
        self.emitted.append(record)


_win32com_logger = None


def setup_test_logger():
    old_log = getattr(win32com, "logger", None)
    global _win32com_logger
    if _win32com_logger is None:
        _win32com_logger = logging.Logger("test")
        handler = LogHandler()
        _win32com_logger.addHandler(handler)

    win32com.logger = _win32com_logger
    handler = _win32com_logger.handlers[0]
    handler.emitted = []
    return handler.emitted, old_log


def restore_test_logger(prev_logger):
    assert prev_logger is None, "who needs this?"
    if prev_logger is None:
        del win32com.logger
    else:
        win32com.logger = prev_logger


# We used to override some of this (and may later!)
TestCase = unittest.TestCase


def CapturingFunctionTestCase(*args, **kw):
    real_test = _CapturingFunctionTestCase(*args, **kw)
    return LeakTestCase(real_test)


class _CapturingFunctionTestCase(unittest.FunctionTestCase):  # , TestCaseMixin):
    def __call__(self, result=None):
        if result is None:
            result = self.defaultTestResult()
        writer = CaptureWriter()
        # self._preTest()
        writer.capture()
        try:
            unittest.FunctionTestCase.__call__(self, result)
            if getattr(self, "do_leak_tests", 0) and hasattr(sys, "gettotalrefcount"):
                self.run_leak_tests(result)
        finally:
            writer.release()
            # self._postTest(result)
        output = writer.get_captured()
        self.checkOutput(output, result)
        if result.showAll:
            print(output)

    def checkOutput(self, output, result):
        if output.find("Traceback") >= 0:
            msg = "Test output contained a traceback\n---\n%s\n---" % output
            result.errors.append((self, msg))


class ShellTestCase(unittest.TestCase):
    def __init__(self, cmd, expected_output):
        self.__cmd = cmd
        self.__eo = expected_output
        unittest.TestCase.__init__(self)

    def runTest(self):
        ExecuteShellCommand(self.__cmd, self, self.__eo)

    def __str__(self):
        max = 30
        if len(self.__cmd) > max:
            cmd_repr = self.__cmd[:max] + "..."
        else:
            cmd_repr = self.__cmd
        return "exec: " + cmd_repr


def testmain(*args, **kw):
    pywin32_testutil.testmain(*args, **kw)
    CheckClean()
