#!/usr/bin/env python3
"""
WinMsg Tester v1.0.0
wireandbrain.com — free tools from the bench

Test whether text and keystrokes can be injected into any Windows application
window. Useful for diagnosing UI automation issues and understanding Windows
User Interface Privilege Isolation (UIPI) security boundaries.

Three injection methods tested, top to bottom:
  A. SendInput  — OS-level injection, most reliable for modern apps
  B. PostMessage WM_CHAR  — classic Win32, works on most native controls
  C. WM_SETTEXT  — sets edit control text directly

Requirements: Python 3.9+, Windows only (uses ctypes + win32 APIs)
No external libraries needed.

Usage: python winmsg_tester.py

License: MIT — free to use, modify, and distribute.
"""

import tkinter as tk
from tkinter import ttk, scrolledtext
import ctypes
import ctypes.wintypes
import time

U32 = ctypes.windll.user32

# Win32 message constants
WM_CHAR    = 0x0102
WM_KEYDOWN = 0x0100
WM_KEYUP   = 0x0101
WM_SETTEXT = 0x000C
VK_RETURN  = 0x0D
VK_LBUTTON = 0x01

# SendInput structures
INPUT_KEYBOARD    = 1
KEYEVENTF_UNICODE = 0x0004
KEYEVENTF_KEYUP   = 0x0002


class KEYBDINPUT(ctypes.Structure):
    _fields_ = [
        ("wVk",         ctypes.wintypes.WORD),
        ("wScan",       ctypes.wintypes.WORD),
        ("dwFlags",     ctypes.wintypes.DWORD),
        ("time",        ctypes.wintypes.DWORD),
        ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
    ]


class _INPUT_UNION(ctypes.Union):
    _fields_ = [("ki", KEYBDINPUT)]


class INPUT(ctypes.Structure):
    _fields_ = [("type", ctypes.wintypes.DWORD), ("_input", _INPUT_UNION)]


def _send_input_text(text: str, send_enter: bool = False) -> int:
    """
    Inject text using SendInput — operates below the Win32 message queue.
    Most reliable method for Electron, WPF, and other framework-hosted apps.
    Returns number of input events successfully injected.

    Note: Will be blocked by UIPI if target process runs at higher integrity
    level (e.g., apps running as Administrator when this tool is not).
    """
    inputs = []
    for ch in text:
        scan = ord(ch)
        for flags in (KEYEVENTF_UNICODE, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP):
            ki = KEYBDINPUT(wVk=0, wScan=scan, dwFlags=flags,
                            time=0, dwExtraInfo=None)
            inputs.append(INPUT(type=INPUT_KEYBOARD,
                                _input=_INPUT_UNION(ki=ki)))
    if send_enter:
        for flags in (0, KEYEVENTF_KEYUP):
            ki = KEYBDINPUT(wVk=VK_RETURN, wScan=0, dwFlags=flags,
                            time=0, dwExtraInfo=None)
            inputs.append(INPUT(type=INPUT_KEYBOARD,
                                _input=_INPUT_UNION(ki=ki)))
    arr = (INPUT * len(inputs))(*inputs)
    return U32.SendInput(len(inputs), arr, ctypes.sizeof(INPUT))


def _get_window_title(hwnd: int) -> str:
    buf = ctypes.create_unicode_buffer(256)
    U32.GetWindowTextW(hwnd, buf, 256)
    return buf.value


def _hwnd_at_cursor() -> tuple:
    pt = ctypes.wintypes.POINT()
    U32.GetCursorPos(ctypes.byref(pt))
    hwnd = U32.WindowFromPoint(pt)
    return hwnd, _get_window_title(hwnd)


class WinMsgTester(tk.Tk):

    def __init__(self):
        super().__init__()
        self.title("WinMsg Tester v1.0.0  —  wireandbrain.com")
        self.resizable(False, False)
        self.attributes("-topmost", True)

        self.target_hwnd = None
        self._picking    = False
        self._btn_was_up = True

        self._build_ui()
        self._poll_cursor()

    def _build_ui(self):
        PAD = dict(padx=8, pady=4)

        # ── UIPI notice ─────────────────────────────────────────────────────
        note = ttk.Label(
            self,
            text=(
                "ℹ  UIPI note: Electron, Chrome, and elevated-privilege apps block"
                " all three methods by design. That is expected — not a bug."
            ),
            foreground="#888", wraplength=640, justify="left"
        )
        note.grid(row=0, column=0, columnspan=2, sticky="w", padx=10, pady=(6, 0))

        # ── Pick target ─────────────────────────────────────────────────────
        frm = ttk.LabelFrame(self, text="1. Pick Target Window")
        frm.grid(row=1, column=0, columnspan=2, sticky="ew", **PAD)

        ttk.Label(frm, text="HWND:").grid(row=0, column=0, sticky="w")
        self.hwnd_var  = tk.StringVar(value="(none)")
        self.title_var = tk.StringVar(value="")
        ttk.Label(frm, textvariable=self.hwnd_var,  width=12, relief="sunken") \
            .grid(row=0, column=1, sticky="w", padx=4)
        ttk.Label(frm, textvariable=self.title_var, width=42, relief="sunken") \
            .grid(row=0, column=2, sticky="w", padx=4)

        self.pick_btn = ttk.Button(frm, text="⊕  Pick Window",
                                   command=self._start_pick)
        self.pick_btn.grid(row=0, column=3, padx=8)

        self.hover_var = tk.StringVar(
            value="Click 'Pick Window', hover over any app window, then left-click to capture.")
        ttk.Label(frm, textvariable=self.hover_var, foreground="gray", width=74) \
            .grid(row=1, column=0, columnspan=4, sticky="w", pady=(0, 4))

        # ── Message ─────────────────────────────────────────────────────────
        frm2 = ttk.LabelFrame(self, text="2. Message")
        frm2.grid(row=2, column=0, columnspan=2, sticky="ew", **PAD)

        self.msg_var = tk.StringVar(value="Hello from WinMsg Tester")
        ttk.Label(frm2, text="Text:").grid(row=0, column=0, sticky="w")
        ttk.Entry(frm2, textvariable=self.msg_var, width=50) \
            .grid(row=0, column=1, padx=6, pady=4)
        self.enter_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(frm2, text="Append Enter", variable=self.enter_var) \
            .grid(row=0, column=2, padx=4)

        # ── Send methods ────────────────────────────────────────────────────
        frm3 = ttk.LabelFrame(
            self, text="3. Send Method  (try A first — fall back to B or C if needed)")
        frm3.grid(row=3, column=0, columnspan=2, sticky="ew", **PAD)

        btns = [
            ("A: SendInput  (OS-level, best for modern apps)",
             self._do_send_input,
             "Injects below Win32 queue. Brings window to foreground first."),
            ("B: PostMessage WM_CHAR",
             self._do_wm_char,
             "Classic Win32. Works on most native Edit controls. Text only — Enter via WM_KEYDOWN."),
            ("C: WM_SETTEXT  (replaces field contents)",
             self._do_settext,
             "Sets text of target control directly. No keystroke simulation."),
        ]
        for i, (label, cmd, hint) in enumerate(btns):
            ttk.Button(frm3, text=label, command=cmd) \
                .grid(row=i, column=0, padx=6, pady=4, sticky="w")
            ttk.Label(frm3, text=hint, foreground="gray") \
                .grid(row=i, column=1, sticky="w", padx=4)

        # ── Log ─────────────────────────────────────────────────────────────
        frm4 = ttk.LabelFrame(self, text="Result Log")
        frm4.grid(row=4, column=0, columnspan=2, sticky="ew", **PAD)
        self.log = scrolledtext.ScrolledText(
            frm4, height=10, width=80, state="disabled", font=("Consolas", 9))
        self.log.grid(row=0, column=0, padx=4, pady=4)
        ttk.Button(frm4, text="Clear", command=self._clear_log) \
            .grid(row=1, column=0, sticky="e", padx=4, pady=(0, 4))

        # ── Footer ──────────────────────────────────────────────────────────
        ttk.Label(self, text="wireandbrain.com  |  v1.0.0  |  MIT License",
                  foreground="#777", font=("", 8)) \
            .grid(row=5, column=0, columnspan=2, pady=(2, 6))

    # ── CURSOR POLL ─────────────────────────────────────────────────────────

    def _poll_cursor(self):
        if self._picking:
            hwnd, title = _hwnd_at_cursor()
            self.hover_var.set(
                f"Under cursor → {hex(hwnd)}  "
                f"[{(title or '(no title)')[:55]}]   LEFT-CLICK to capture")

            btn_down = bool(U32.GetAsyncKeyState(VK_LBUTTON) & 0x8000)
            if self._btn_was_up and btn_down and hwnd != self.winfo_id():
                self._capture(hwnd, title)
                self._picking    = False
                self._btn_was_up = True
                self.pick_btn.config(state="normal")
            self._btn_was_up = not btn_down

        self.after(80, self._poll_cursor)

    def _start_pick(self):
        self._picking    = True
        self._btn_was_up = False
        self.pick_btn.config(state="disabled")
        self.hover_var.set("Move cursor to target window and LEFT-CLICK to capture...")

    def _capture(self, hwnd: int, title: str):
        self.target_hwnd = hwnd
        self.hwnd_var.set(hex(hwnd))
        self.title_var.set((title or "(no title)")[:50])
        self._log(f"[PICK]  HWND {hex(hwnd)}  —  {(title or '(no title)')[:70]}")

    # ── SEND METHODS ────────────────────────────────────────────────────────

    def _get_target(self):
        if not self.target_hwnd:
            self._log("[ERROR] No target window selected. Click 'Pick Window' first.")
            return None
        return self.target_hwnd

    def _do_send_input(self):
        hwnd = self._get_target()
        if hwnd is None: return
        msg, enter = self.msg_var.get(), self.enter_var.get()
        try:
            U32.SetForegroundWindow(hwnd)
            self.after(200, lambda: self._send_input_deferred(hwnd, msg, enter))
        except Exception as e:
            self._log(f"[SendInput] SetForegroundWindow error: {e}")

    def _send_input_deferred(self, hwnd, msg, enter):
        try:
            sent = _send_input_text(msg, send_enter=enter)
            suffix = " + Enter" if enter else ""
            self._log(
                f"[A: SendInput]   → {hex(hwnd)}  |  "
                f"'{msg}'{suffix}  |  {sent} events injected")
        except Exception as e:
            self._log(f"[A: SendInput] ERROR: {e}")

    def _do_wm_char(self):
        hwnd = self._get_target()
        if hwnd is None: return
        msg, enter = self.msg_var.get(), self.enter_var.get()
        try:
            for ch in msg:
                U32.PostMessageW(hwnd, WM_CHAR, ord(ch), 1)
            if enter:
                U32.PostMessageW(hwnd, WM_KEYDOWN, VK_RETURN, 1)
                U32.PostMessageW(hwnd, WM_KEYUP,   VK_RETURN, 1)
            suffix = " + Enter" if enter else ""
            self._log(
                f"[B: WM_CHAR]     → {hex(hwnd)}  |  "
                f"'{msg}'{suffix}  |  posted")
        except Exception as e:
            self._log(f"[B: WM_CHAR] ERROR: {e}")

    def _do_settext(self):
        hwnd = self._get_target()
        if hwnd is None: return
        msg = self.msg_var.get()
        try:
            result = U32.SendMessageW(hwnd, WM_SETTEXT, 0, msg)
            self._log(
                f"[C: WM_SETTEXT]  → {hex(hwnd)}  |  "
                f"'{msg}'  |  result={result}")
        except Exception as e:
            self._log(f"[C: WM_SETTEXT] ERROR: {e}")

    # ── LOG ─────────────────────────────────────────────────────────────────

    def _log(self, msg: str):
        ts = time.strftime("%H:%M:%S")
        self.log.config(state="normal")
        self.log.insert("end", f"[{ts}] {msg}\n")
        self.log.see("end")
        self.log.config(state="disabled")

    def _clear_log(self):
        self.log.config(state="normal")
        self.log.delete("1.0", "end")
        self.log.config(state="disabled")


if __name__ == "__main__":
    app = WinMsgTester()
    app.mainloop()
