import sys
import unittest

import pythoncom
import win32com.server.policy
import win32com.test.util
from win32com.axscript import axscript
from win32com.axscript.server import axsite
from win32com.client.dynamic import Dispatch
from win32com.server import connect, util

verbose = "-v" in sys.argv


class MySite(axsite.AXSite):
    def __init__(self, *args):
        self.exception_seen = None
        axsite.AXSite.__init__(self, *args)

    def OnScriptError(self, error):
        self.exception_seen = exc = error.GetExceptionInfo()
        context, line, char = error.GetSourcePosition()
        if not verbose:
            return
        print(" >Exception:", exc[1])
        try:
            st = error.GetSourceLineText()
        except pythoncom.com_error:
            st = None
        if st is None:
            st = ""
        text = st + "\n" + (" " * (char - 1)) + "^" + "\n" + exc[2]
        for line in text.splitlines():
            print("  >" + line)


class MyCollection(util.Collection):
    def _NewEnum(self):
        return util.Collection._NewEnum(self)


class Test:
    _public_methods_ = ["echo", "fail"]
    _public_attrs_ = ["collection"]

    def __init__(self):
        self.verbose = verbose
        self.collection = util.wrap(MyCollection([1, "Two", 3]))
        self.last = ""
        self.fail_called = 0

    #    self._connect_server_ = TestConnectServer(self)

    def echo(self, *args):
        self.last = "".join([str(s) for s in args])
        if self.verbose:
            for arg in args:
                print(arg, end=" ")
            print()

    def fail(self, *args):
        print("**** fail() called ***")
        for arg in args:
            print(arg, end=" ")
        print()
        self.fail_called = 1


#    self._connect_server_.Broadcast(last)


#### Connections currently won't work, as there is no way for the engine to
#### know what events we support.  We need typeinfo support.

IID_ITestEvents = pythoncom.MakeIID("{8EB72F90-0D44-11d1-9C4B-00AA00125A98}")


class TestConnectServer(connect.ConnectableServer):
    _connect_interfaces_ = [IID_ITestEvents]

    # The single public method that the client can call on us
    # (ie, as a normal COM server, this exposes just this single method.
    def __init__(self, object):
        self.object = object

    def Broadcast(self, arg):
        # Simply broadcast a notification.
        self._BroadcastNotify(self.NotifyDoneIt, (arg,))

    def NotifyDoneIt(self, interface, arg):
        interface.Invoke(1000, 0, pythoncom.DISPATCH_METHOD, 1, arg)


VBScript = """\
prop = "Property Value"

sub hello(arg1)
   test.echo arg1
end sub

sub testcollection
   if test.collection.Item(0) <> 1 then
     test.fail("Index 0 was wrong")
   end if
   if test.collection.Item(1) <> "Two" then
     test.fail("Index 1 was wrong")
   end if
   if test.collection.Item(2) <> 3 then
     test.fail("Index 2 was wrong")
   end if
   num = 0
   for each item in test.collection
     num = num + 1
   next
   if num <> 3 then
     test.fail("Collection didn't have 3 items")
   end if
end sub
"""
PyScript = """\
# A unicode \xa9omment.
prop = "Property Value"
def hello(arg1):
   test.echo(arg1)

def testcollection():
#   test.collection[1] = "New one"
   got = []
   for item in test.collection:
     got.append(item)
   if got != [1, "Two", 3]:
     test.fail("Didn't get the collection")
   pass
"""

ErrScript = """\
bad code for everyone!
"""

state_map = {
    axscript.SCRIPTSTATE_UNINITIALIZED: "SCRIPTSTATE_UNINITIALIZED",
    axscript.SCRIPTSTATE_INITIALIZED: "SCRIPTSTATE_INITIALIZED",
    axscript.SCRIPTSTATE_STARTED: "SCRIPTSTATE_STARTED",
    axscript.SCRIPTSTATE_CONNECTED: "SCRIPTSTATE_CONNECTED",
    axscript.SCRIPTSTATE_DISCONNECTED: "SCRIPTSTATE_DISCONNECTED",
    axscript.SCRIPTSTATE_CLOSED: "SCRIPTSTATE_CLOSED",
}


def _CheckEngineState(engine, name, state):
    got = engine.engine.eScript.GetScriptState()
    if got != state:
        got_name = state_map.get(got, str(got))
        state_name = state_map.get(state, str(state))
        raise RuntimeError(
            f"Warning - engine {name} has state {got_name}, but expected {state_name}"
        )


class EngineTester(win32com.test.util.TestCase):
    def _TestEngine(self, engineName, code, expected_exc=None):
        echoer = Test()
        model = {
            "test": util.wrap(echoer),
        }
        site = MySite(model)
        engine = site._AddEngine(engineName)
        try:
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_INITIALIZED)
            engine.AddCode(code)
            engine.Start()
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_STARTED)
            self.assertTrue(not echoer.fail_called, "Fail should not have been called")
            # Now call into the scripts IDispatch
            ob = Dispatch(engine.GetScriptDispatch())
            try:
                ob.hello("Goober")
                self.assertTrue(
                    expected_exc is None,
                    f"Expected {expected_exc!r}, but no exception seen",
                )
            except pythoncom.com_error:
                if expected_exc is None:
                    self.fail(
                        f"Unexpected failure from script code: {site.exception_seen}"
                    )
                if expected_exc not in site.exception_seen[2]:
                    self.fail(
                        f"Could not find {expected_exc!r} in {site.exception_seen[2]!r}"
                    )
                return
            self.assertEqual(echoer.last, "Goober")

            self.assertEqual(str(ob.prop), "Property Value")
            ob.testcollection()
            self.assertTrue(not echoer.fail_called, "Fail should not have been called")

            # Now make sure my engines can evaluate stuff.
            result = engine.eParse.ParseScriptText(
                "1+1", None, None, None, 0, 0, axscript.SCRIPTTEXT_ISEXPRESSION
            )
            self.assertEqual(result, 2)
            # re-initialize to make sure it transitions back to initialized again.
            engine.SetScriptState(axscript.SCRIPTSTATE_INITIALIZED)
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_INITIALIZED)
            engine.Start()
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_STARTED)

            # Transition back to initialized, then through connected too.
            engine.SetScriptState(axscript.SCRIPTSTATE_INITIALIZED)
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_INITIALIZED)
            engine.SetScriptState(axscript.SCRIPTSTATE_CONNECTED)
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_CONNECTED)
            engine.SetScriptState(axscript.SCRIPTSTATE_INITIALIZED)
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_INITIALIZED)

            engine.SetScriptState(axscript.SCRIPTSTATE_CONNECTED)
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_CONNECTED)
            engine.SetScriptState(axscript.SCRIPTSTATE_DISCONNECTED)
            _CheckEngineState(site, engineName, axscript.SCRIPTSTATE_DISCONNECTED)
        finally:
            engine.Close()
            engine = None
            site = None

    def testVB(self):
        self._TestEngine("VBScript", VBScript)

    def testPython(self):
        self._TestEngine("Python", PyScript)

    def testPythonUnicodeError(self):
        self._TestEngine("Python", PyScript)

    def testVBExceptions(self):
        self.assertRaises(pythoncom.com_error, self._TestEngine, "VBScript", ErrScript)


if __name__ == "__main__":
    unittest.main()
