import os
import random
import re
import shutil
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Type, TypeVar, List

import wx

from clarabot_nano.common.spawn.constants import ShortNames
from clarabot_nano.control.app import ControlApplication
from clarabot_nano.control.client import WxCoreInterface, WxCoreBuffer
from clarabot_nano.control.page.login import ControlLogin
from clarabot_nano.control.utility import find_children_of_type
from clarabot_nano.control.window import ControlWindow
from clarabot_nano.development.spawn.common import write_spawn_debug_info
from clarabot_nano.utility.path.resolve import resolve_path

_TwxDialog = TypeVar("_TwxDialog", bound=wx.Dialog)
_TwxWindow = TypeVar("_TwxWindow", bound=wx.Window)


class ControlApplicationTesterBase(ControlApplication):
    @contextmanager
    def _setup(self, core_interface: WxCoreInterface):
        config_dir = resolve_path(core_interface.get_config()["local"]["config_dir"], strict=True)
        write_spawn_debug_info(config_dir, os.getpid(), ShortNames.CONTROL)
        with ControlApplication._setup(self, core_interface) as result:
            yield result


def phase_timeout(t: float):
    def w(fn):
        fn.phase_timeout = t
        return fn

    return w


class ControlApplicationTester(ControlApplicationTesterBase):

    TEST_PHASE_METHOD_NAME_RE = re.compile(r"^_phase_(\d+)$")

    def __init__(
        self,
        core_buffer: WxCoreBuffer,
        tester_communication_dir: Path,
        disable_systray: bool = False,
        phase_interval: int = 250,
        **kwargs,
    ):
        assert not kwargs
        ControlApplicationTesterBase.__init__(self, core_buffer)
        self.tester_communication_dir = tester_communication_dir
        self.test_log = None
        self.disable_systray = disable_systray
        self.window = None
        self.tester_timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self._on_tester_interval, self.tester_timer)
        self.timer_phase_interval = phase_interval
        self._tester_phases = []
        phases = dict()
        for name in dir(self):
            m = self.TEST_PHASE_METHOD_NAME_RE.match(name)
            if m:
                num = int(m.group(1))
                assert num not in phases, f"{name} {num}"
                phases[num] = getattr(self, name)
        phase_keys = sorted(phases.keys())
        assert phase_keys == list(range(len(phases)))
        self._tester_phases = [phases[k] for k in phase_keys]
        self._tester_phase = 0
        self._tester_phase_time = 0.0
        self._phase_timeout = 0.0

    def _on_tester_interval(self, event):
        assert isinstance(self.window, ControlWindow), str(self.window)
        if self._tester_phase == len(self._tester_phases):
            self.write_test_log("Test phases passed.")
            self.window.request_shutdown()
            self.tester_timer.Stop()
        elif self._tester_phase < len(self._tester_phases):
            method = self._tester_phases[self._tester_phase]
            try:
                if r := method(self.window):
                    if isinstance(r, bool):
                        self.write_test_log(f"Phase {self._tester_phase} passed.")
                        self._tester_phase += 1
                    elif isinstance(r, int):
                        self.write_test_log(f"Phase {self._tester_phase} passed, jumping to {r}.")
                        assert 0 <= r <= len(self._tester_phases), str(r)
                        self._tester_phase = r
                    self._tester_phase_time = time.perf_counter()
            except AssertionError as e:
                if hasattr(method, "phase_timeout"):
                    time_limit = method.phase_timeout
                else:
                    time_limit = self._phase_timeout
                if time.perf_counter() - self._tester_phase_time > time_limit:
                    self.write_test_log(str(e))
                    self.window.request_shutdown()
                    self.tester_timer.Stop()
                    raise e
        else:
            assert False, f"{self._tester_phase} {self._tester_phases}"

    def write_test_log(self, text):
        assert self.test_log is not None
        self.test_log.write(text)
        self.test_log.write("\n")
        self.test_log.flush()

    def _create_window(self, core_interface: WxCoreInterface, *args, **kwargs) -> ControlWindow:
        self.window = ControlApplicationTesterBase._create_window(
            self, core_interface, *args, disable_tray_icon=self.disable_systray, **kwargs
        )
        self.tester_timer.Start(self.timer_phase_interval)
        self._tester_phase_time = time.perf_counter()
        if self.disable_systray:
            assert self.window.tray_icon is None
            self.write_test_log("Systray disabled for testing.")
        else:
            self.write_test_log(f"Systray available: {bool(self.window.tray_icon)}")
        return self.window

    def OnRun(self):
        with (self.tester_communication_dir / "test_log").open("w", encoding="utf-8", newline="\n") as f:
            self.test_log = f
            try:
                self.write_test_log(f"Starting {self.__class__.__name__}")
                return ControlApplicationTesterBase.OnRun(self)
            except Exception as e:
                self.write_test_log(f"Run-exception {e}")
                raise e
            finally:
                self.tester_timer.Stop()
                self.write_test_log("Exiting")
                self.test_log = None
                # move to done
                done_path = self.tester_communication_dir / "test_log.done"
                if not done_path.exists():
                    shutil.copy(self.tester_communication_dir / "test_log", done_path)

    # utilities
    def _move_window_around(self, w: wx.Window):
        assert w.IsShown()
        screen_geo: wx.Rect = wx.Display().GetGeometry()
        window_geo: wx.Rect = w.GetScreenRect()
        tx = max(5, int((screen_geo.GetWidth() - window_geo.GetWidth() - 10) * random.random()))
        # NOTE: Extra large height limitation is to avoid unhandled offsets from window-headers. They are not handled
        # gracefully on all platforms.
        ty = max(5, int((screen_geo.GetHeight() - window_geo.GetHeight() - 120) * random.random()))
        w.Move(screen_geo.GetX() + tx, screen_geo.GetY() + ty)
        return w.GetScreenRect()

    def _check_waiting_login_get_central_widget(self, w: ControlWindow) -> ControlLogin:
        config = w.core_interface.get_config()
        assert (v := config["state"].get("gateway_status")) == "waiting-config", str(v)
        assert w.IsShown()
        central_widget = w.central_widget()
        assert isinstance(central_widget, ControlLogin)
        assert w._inhibits_sleep is False
        return central_widget

    def _get_text_entry_dialog(self, w: wx.Window, require=True) -> wx.TextEntryDialog:
        return self._get_modal_dialog(w, wx.TextEntryDialog, require=require)

    def _get_modal_dialog(self, w: wx.Window, cls: Type[_TwxDialog], require=True) -> _TwxDialog:
        ws = find_children_of_type(w, cls)
        assert len(ws) <= 1, str(ws)
        if require:
            assert ws
        if ws:
            assert ws[0].IsModal()
            assert ws[0].IsShown()
            return ws[0]

    def _find_children_of_id(self, parent: wx.Window, wx_id: int) -> List[_TwxWindow]:
        c = []
        for w in parent.GetChildren():
            if w.GetId() == wx_id:
                c.append(w)
            else:
                c.extend(self._find_children_of_id(w, wx_id))
        return c

    def _find_send_command_event(self, parent: wx.Window, wx_id: int, evt: int):
        ws = self._find_children_of_id(parent, wx_id)
        assert len(ws) == 1, str(ws)
        event = wx.CommandEvent(evt, wx_id)
        event.SetEventObject(ws[0])
        parent.ProcessEvent(event)
