"""Three things the page says about packed apps, tested for real. (Windows only)

    python app_behaviour.py errors     what you see when a windowed app fails
    python app_behaviour.py unpack     where a one-file app unpacks itself
    python app_behaviour.py offline    does the game with the model use the network?

Run it with the Python that has PyInstaller: the one in your venv.

"errors" writes two tiny programs into checks/made/, builds each with --windowed, and
starts it. The first divides by zero in its main part. The second opens its window
and then divides by zero in a second thread. For each one it prints the title of
every window that appears in eight seconds, and then closes them.

"unpack" builds hello.py with --onefile --windowed and starts it. While the window
is open it looks in the TEMP folder for the folder the app unpacked itself into,
counts what is in it, and looks again after the app has closed.

"offline" builds nothing. It starts the game that "builds.py model" made, and for 40
seconds it asks Windows, several times a second, which network connections belong
to that program (with the netstat command). It prints how often it looked and every
connection it saw. A connection that opens and closes between two looks is missed.
"""
import ctypes
import ctypes.wintypes
import glob
import os
import pathlib
import shutil
import subprocess
import sys
import time

CHECKS = pathlib.Path(__file__).resolve().parent
ASSETS = CHECKS.parent                           # hello.py: one folder up from checks/
MADE = CHECKS / "made"
WM_CLOSE = 0x0010                                # what Windows sends when you click the X
BLOCKED = 4551                                   # Smart App Control said no

MAIN_FAILS = '''import tkinter as tk


def main():
    root = tk.Tk()
    root.title("main_fails")
    number = 1 / 0
    root.mainloop()


if __name__ == "__main__":
    main()
'''

THREAD_FAILS = '''import threading
import time
import tkinter as tk


def work():
    time.sleep(1)
    number = 1 / 0


def main():
    root = tk.Tk()
    root.title("thread_fails")
    tk.Label(root, text="The window is fine.", font=("Arial", 16)).pack(padx=30, pady=30)
    thread = threading.Thread(target=work, daemon=True)
    thread.start()
    root.mainloop()


if __name__ == "__main__":
    main()
'''


def build(script, options):
    """Build one script into checks/made/<its name>/. Give back the .exe."""
    out = MADE / ("behaviour_" + script.stem)
    shutil.rmtree(out, ignore_errors=True)
    command = [sys.executable, "-m", "PyInstaller", "--noconfirm",
               "--distpath", str(out / "dist"), "--workpath", str(out / "work"),
               "--specpath", str(out)] + options + [str(script)]
    done = subprocess.run(command, capture_output=True, text=True)
    if done.returncode != 0:
        print(done.stderr[-1500:])
        raise SystemExit("PyInstaller failed on " + script.name)
    exe = out / "dist" / (script.stem + ".exe")
    if not exe.exists():
        exe = out / "dist" / script.stem / (script.stem + ".exe")
    return exe


def start(exe):
    """Start an app. A new .exe can be blocked, so build-and-start is tried by the caller."""
    try:
        return subprocess.Popen([str(exe)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    except OSError as problem:
        if problem.winerror == BLOCKED:
            return None
        raise


def windows_of(pid):
    """The title of every visible window that belongs to this process."""
    user32 = ctypes.windll.user32
    found = {}

    @ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)
    def each(handle, _):
        owner = ctypes.wintypes.DWORD()
        user32.GetWindowThreadProcessId(handle, ctypes.byref(owner))
        if owner.value == pid and user32.IsWindowVisible(handle):
            title = ctypes.create_unicode_buffer(300)
            user32.GetWindowTextW(handle, title, 300)
            if title.value:
                found[handle] = title.value
        return True

    user32.EnumWindows(each, 0)
    return found


def first_line_of_box(handle):
    """The words in an error box: the text of its first two labels."""
    user32 = ctypes.windll.user32
    texts = []

    @ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)
    def each(child, _):
        text = ctypes.create_unicode_buffer(600)
        user32.GetWindowTextW(child, text, 600)
        if text.value.strip() and text.value != "Close":
            texts.append(" ".join(text.value.split()))
        return True

    user32.EnumChildWindows(handle, each, 0)
    return texts[:2]


def errors():
    MADE.mkdir(exist_ok=True)
    for name, source in (("main_fails", MAIN_FAILS), ("thread_fails", THREAD_FAILS)):
        script = MADE / (name + ".py")
        script.write_text(source, encoding="utf-8")
        program = None
        for attempt in range(5):                 # a blocked build is built again
            program = start(build(script, ["--windowed"]))
            if program:
                break
        if not program:
            print(name, ": Smart App Control blocked all five builds")
            continue
        seen = {}
        began = time.time()
        while time.time() - began < 8:
            for handle, title in windows_of(program.pid).items():
                if handle not in seen:
                    seen[handle] = title
                    time.sleep(0.5)
                    words = first_line_of_box(handle) if title != name else []
                    print(f"{name}: a window named {title!r} appeared" + (f". It says: {words}" if words else ""))
            time.sleep(0.2)
        if not any(title != name for title in seen.values()):
            print(f"{name}: no error box appeared in 8 seconds.")
        for handle in seen:
            ctypes.windll.user32.PostMessageW(handle, WM_CLOSE, 0, 0)
        try:
            program.wait(timeout=20)
        except subprocess.TimeoutExpired:
            program.kill()
    return 0


def unpack():
    temp = os.environ["TEMP"]
    pattern = os.path.join(temp, "_MEI*")
    program = None
    for attempt in range(5):
        before = set(glob.glob(pattern))
        program = start(build(ASSETS / "hello.py", ["--onefile", "--windowed"]))
        if program:
            break
    if not program:
        print("Smart App Control blocked all five builds")
        return 1
    while not ctypes.windll.user32.FindWindowW(None, "hello"):
        time.sleep(0.05)
    time.sleep(1)
    for folder in set(glob.glob(pattern)) - before:
        files = [pathlib.Path(root, name) for root, _, names in os.walk(folder) for name in names]
        size = sum(file.stat().st_size for file in files)
        print(f"While the app is open: a folder named _MEI plus some letters is in TEMP, "
              f"with {len(files)} files and {size / 1e6:.1f} MB in it.")
    ctypes.windll.user32.PostMessageW(ctypes.windll.user32.FindWindowW(None, "hello"), WM_CLOSE, 0, 0)
    program.wait(timeout=30)
    time.sleep(1)
    left = set(glob.glob(pattern)) - before
    print(f"After the app has closed: {len(left)} of those folders are left.")
    return 0


def offline():
    exe = MADE / "pond_with_the_model_one_folder" / "dist" / "pond" / "pond.exe"
    if not exe.exists():
        print("There is no game with the model in checks/made/. Run: python builds.py model")
        return 1
    program = start(exe)
    if not program:
        print("Smart App Control blocked the app")
        return 1
    while not ctypes.windll.user32.FindWindowW(None, "pond"):
        time.sleep(0.05)
    seen = set()
    looks = 0
    began = time.time()
    while time.time() - began < 40:
        table = subprocess.run(["netstat", "-ano"], capture_output=True, text=True).stdout
        looks += 1
        for line in table.splitlines():
            words = line.split()
            # A line is: TCP or UDP, this computer's end, the other end, (a state,) the program's number.
            if len(words) >= 4 and words[0] in ("TCP", "UDP") and words[-1] == str(program.pid):
                seen.add(" ".join(words[:3]))
        time.sleep(0.25)
    ctypes.windll.user32.PostMessageW(ctypes.windll.user32.FindWindowW(None, "pond"), WM_CLOSE, 0, 0)
    try:
        program.wait(timeout=30)
    except subprocess.TimeoutExpired:
        program.kill()
    print(f"The game with the model ran for 40 seconds. We looked {looks} times.")
    for connection in sorted(seen):
        print("   a connection:", connection)
    print(f"Network connections that belonged to the game: {len(seen)}.")
    return 0


def main():
    parts = {"errors": errors, "unpack": unpack, "offline": offline}
    if sys.platform != "win32" or len(sys.argv) != 2 or sys.argv[1] not in parts:
        print(__doc__)
        return 2
    return parts[sys.argv[1]]()


if __name__ == "__main__":
    sys.exit(main())
