"""How often does Windows block a brand-new app? (Windows 11 with Smart App Control on)

    python smart_app_control.py          20 builds of hello.py
    python smart_app_control.py 40       40 builds
    python smart_app_control.py again    build nothing: try the apps of the last run again

Run it with the Python that has PyInstaller: the one in your venv. It builds
hello.py again and again, half the builds as one folder and half as one file. Every
build makes a new .exe that no computer has seen before. It tries to start each one,
and counts how many Windows blocked. A blocked app is tried again one second later,
and once more when all the builds are done.

"again" is for the next day. Windows says it will review a blocked app, so an app
that was blocked can run later. It starts every app the last run left in
checks/made/ and counts again. It prints when each app was built.

Where Smart App Control is off, nothing is blocked and this prints 0.
"""
import ctypes
import pathlib
import platform
import shutil
import subprocess
import sys
import time
import winreg

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                                   # "An Application Control policy has blocked this file"
STATES = {0: "off", 1: "on", 2: "evaluation"}


def smart_app_control():
    """What Windows itself says: is Smart App Control on, off, or in evaluation?"""
    try:
        key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
                             r"SYSTEM\CurrentControlSet\Control\CI\Policy")
        state = winreg.QueryValueEx(key, "VerifiedAndReputablePolicyState")[0]
    except OSError:
        return "not on this version of Windows"
    return STATES.get(state, "state " + str(state))


def try_start(exe):
    """Start the app. Give back "ran" or "blocked". Close its window again."""
    try:
        program = subprocess.Popen([str(exe)], stdout=subprocess.DEVNULL,
                                   stderr=subprocess.DEVNULL)
    except OSError as problem:
        if problem.winerror == BLOCKED:
            return "blocked"
        raise
    began = time.perf_counter()
    while not ctypes.windll.user32.FindWindowW(None, "hello"):
        if program.poll() is not None or time.perf_counter() - began > 120:
            program.kill()
            return "no window"
        time.sleep(0.01)
    time.sleep(0.5)
    ctypes.windll.user32.PostMessageW(ctypes.windll.user32.FindWindowW(None, "hello"),
                                      WM_CLOSE, 0, 0)
    program.wait(timeout=60)
    return "ran"


def build(number, options):
    out = MADE / ("sac" + str(number))
    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(ASSETS / "hello.py")]
    done = subprocess.run(command, capture_output=True, text=True)
    if done.returncode != 0:
        print(done.stderr[-1500:])
        raise SystemExit("PyInstaller failed")
    exe = out / "dist" / "hello.exe"             # --onefile
    if not exe.exists():
        exe = out / "dist" / "hello" / "hello.exe"
    return exe


def the_next_day():
    """Start every app the last run left behind. Count the ones Windows blocks now."""
    apps = sorted(MADE.glob("sac*/dist/hello.exe")) + sorted(MADE.glob("sac*/dist/hello/hello.exe"))
    if not apps:
        print("There are no apps in checks/made/. Run this once without \"again\" first.")
        return 1
    built = [exe.stat().st_mtime for exe in apps]
    print(f"{time.strftime('%Y-%m-%d %H:%M')}  Smart App Control: {smart_app_control()}. "
          f"{len(apps)} apps, built from {time.strftime('%Y-%m-%d %H:%M', time.localtime(min(built)))} "
          f"to {time.strftime('%H:%M', time.localtime(max(built)))}.")
    blocked = [exe for exe in apps if try_start(exe) == "blocked"]
    for exe in blocked:
        print("   still blocked:", exe.relative_to(MADE).as_posix())
    print(f"Now Windows blocks {len(blocked)} of the {len(apps)} apps.")
    return 0


def main():
    if sys.platform != "win32":
        print(__doc__)
        return 2
    if sys.argv[1:] == ["again"]:
        return the_next_day()
    builds = int(sys.argv[1]) if len(sys.argv) > 1 else 20
    import PyInstaller
    print(f"{time.strftime('%Y-%m-%d %H:%M')}  Windows {platform.release()} build {platform.version()}, "
          f"PyInstaller {PyInstaller.__version__}. Smart App Control: {smart_app_control()}.")
    counts = {"one folder": [0, 0], "one file": [0, 0]}          # builds, blocked
    blocked_apps = []
    second_try_ran = 0
    for number in range(1, builds + 1):
        kind = "one folder" if number % 2 else "one file"
        options = [] if kind == "one folder" else ["--onefile"]
        exe = build(number, options)
        first = try_start(exe)
        counts[kind][0] += 1
        line = f"build {number:2d}  {kind:10}  {first}"
        if first == "blocked":
            counts[kind][1] += 1
            time.sleep(1)
            second = try_start(exe)
            line += f"   one second later: {second}"
            if second == "ran":
                second_try_ran += 1
            else:
                blocked_apps.append(exe)
        print(line, flush=True)
    still = sum(try_start(exe) == "blocked" for exe in blocked_apps)
    total = sum(count[0] for count in counts.values())
    blocked = sum(count[1] for count in counts.values())
    for kind, (made, stopped) in counts.items():
        print(f"{kind}: Windows blocked {stopped} of {made} new apps at the first start.")
    print(f"In all: {blocked} of {total} blocked. {second_try_ran} of those ran one second later.")
    print(f"{len(blocked_apps)} were blocked twice. At the end of the run, "
          f"{still} of them were still blocked.")
    return 0


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