#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Yuvin's Test Services — Terminal Edition
========================================
v.1.1.0

YTS began in September 2024 as five little Python scripts in a command
prompt. In 2025 it became a website. In 2026, it comes back home to the
terminal — same name as the very first script: yuvin.py.

Works on macOS Terminal, Linux terminals, and Windows Command Prompt.
No installs needed — just Python 3.6+ and this one file.

Run it:            python3 yuvin.py        (macOS / Linux)
                   python yuvin.py         (Windows)
Options:           --offline    don't talk to yuvin.com.au at all
                   --no-color   plain text, no colors
                   --ascii      no emoji / fancy box characters
                   --version    print the version and exit
"""

import argparse
import json
import os
import random
import shutil
import sys
import threading
import time
import uuid

VERSION = "v.1.1.0"
SITE = "https://yuvin.com.au"
STATE_FILE = os.path.join(os.path.expanduser("~"), ".yuvin_terminal.json")

# ---------------------------------------------------------------- terminal setup

def enable_ansi():
    """Turn on ANSI colors. On Windows CMD this needs a console mode flag."""
    if os.name != "nt":
        return True
    try:
        import ctypes
        kernel32 = ctypes.windll.kernel32
        handle = kernel32.GetStdHandle(-11)  # stdout
        mode = ctypes.c_uint32()
        if kernel32.GetConsoleMode(handle, ctypes.byref(mode)) == 0:
            return False
        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
        return kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0
    except Exception:
        return False


def can_encode(text):
    """Can this terminal actually print these characters?"""
    enc = getattr(sys.stdout, "encoding", None) or "ascii"
    try:
        text.encode(enc)
        return True
    except Exception:
        return False


ARGS = None          # filled in by main()
COLOR = True
UNICODE_OK = True
EMOJI_OK = True


class C:
    """Site palette, translated to ANSI. #2563eb blue is the brand color."""
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    BLUE = "\033[94m"
    CYAN = "\033[96m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    RED = "\033[91m"
    GRAY = "\033[90m"


def col(code, text):
    if not COLOR:
        return text
    return code + text + C.RESET


def sym(emoji, fallback=""):
    """Use the emoji if the terminal can show it, otherwise a plain fallback."""
    if EMOJI_OK:
        return emoji
    return fallback


def to_int(text):
    """int() that returns None instead of crashing — str.isdigit() lies
    about characters like '²' that int() rejects."""
    try:
        return int(text)
    except (ValueError, TypeError):
        return None


ASCII_SWAPS = (("—", "-"), ("·", "."), ("×", "x"), ("÷", "/"), ("●", "*"),
               ("─", "-"), ("│", "|"), ("┌", "+"), ("└", "+"))


def downgrade(text):
    """When the terminal can't do Unicode, swap fancy characters for plain
    ones so print() can never crash with UnicodeEncodeError."""
    if not UNICODE_OK:
        for fancy, plain in ASCII_SWAPS:
            text = text.replace(fancy, plain)
    return text


def width():
    try:
        w = shutil.get_terminal_size((80, 24)).columns
    except Exception:
        w = 80
    return max(56, min(w, 96))


def line(ch=None):
    if ch is None:
        ch = "─" if UNICODE_OK else "-"
    print(col(C.GRAY, ch * width()))


def clear():
    if sys.stdout.isatty():
        os.system("cls" if os.name == "nt" else "clear")


def emit(text):
    """downgrade() plus a guarantee the result can actually be encoded to
    this stdout — even on Python 3.6, where reconfigure() doesn't exist."""
    text = downgrade(text)
    enc = getattr(sys.stdout, "encoding", None)
    if enc:
        try:
            text.encode(enc)
        except Exception:
            text = text.encode(enc, "replace").decode(enc, "replace")
    return text


def say(text=""):
    print(emit(text))


def heading(text):
    say(col(C.BOLD + C.BLUE, text))


def badge(text):
    return col(C.YELLOW + C.BOLD, "[" + text + "]")


def card(title, body_lines):
    """A feature card, like the ones on /home (open on the right side so
    emoji width never breaks the border)."""
    tl = "┌" if UNICODE_OK else "+"
    vl = "│" if UNICODE_OK else "|"
    bl = "└" if UNICODE_OK else "+"
    dash = "─" if UNICODE_OK else "-"
    say(col(C.GRAY, tl + dash + " ") + col(C.BOLD, title))
    for b in body_lines:
        say(col(C.GRAY, vl) + "  " + b)
    say(col(C.GRAY, bl + dash * 3))


def ask_raw(prompt):
    """input() that never crashes the app — Ctrl+C / Ctrl+D exit politely,
    and un-typeable pasted bytes just count as an empty answer. Keeps the
    answer exactly as typed (leading spaces matter for canvas art)."""
    try:
        return input(emit(col(C.CYAN, prompt)))
    except (KeyboardInterrupt, EOFError):
        say("")
        goodbye()
    except UnicodeDecodeError:
        return ""


def ask(prompt):
    return ask_raw(prompt).strip()


def pause():
    ask("\nPress Enter to go back to Home... ")

# ---------------------------------------------------------------- saved progress

def load_state():
    try:
        with open(STATE_FILE, "r") as f:
            data = json.load(f)
        if isinstance(data, dict):
            # keep only sane non-negative whole numbers so a hand-edited or
            # corrupt file can never crash the games' score arithmetic
            return {k: v for k, v in data.items()
                    if isinstance(v, int) and not isinstance(v, bool) and v >= 0}
    except Exception:
        pass
    return {}


def save_state(state):
    try:
        with open(STATE_FILE, "w") as f:
            json.dump(state, f)
    except Exception:
        pass  # progress just won't be saved — never crash over it

# ---------------------------------------------------------------- live presence

CURRENT_PAGE = "Home"
_TAB_ID = "cli-" + uuid.uuid4().hex[:24]
_STOP = threading.Event()


def _urlopen(url_or_req, timeout=4):
    """urlopen that copes with computers whose Python has a broken/missing
    certificate store (common on fresh macOS installs). Tries proper SSL
    first; only if certificate verification itself fails, retries without
    it — this app never sends passwords or secrets, only public pings."""
    import urllib.request
    import ssl
    try:
        return urllib.request.urlopen(url_or_req, timeout=timeout)
    except Exception as e:
        cause = getattr(e, "reason", e)
        # ssl.SSLCertVerificationError only exists on Python 3.7+
        verify_err = getattr(ssl, "SSLCertVerificationError", ())
        if isinstance(cause, verify_err) or "CERTIFICATE_VERIFY_FAILED" in str(e):
            ctx = ssl._create_unverified_context()
            return urllib.request.urlopen(url_or_req, timeout=timeout, context=ctx)
        raise


def _post_json(path, payload, timeout=4):
    import urllib.request
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(SITE + path, data=data,
                                 headers={"Content-Type": "application/json"})
    _urlopen(req, timeout=timeout).read()


def _beacon_loop():
    """Every 25s tell yuvin.com.au this terminal is open — the CLI shows up
    in the Live Tabs console just like a browser tab does."""
    while not _STOP.is_set():
        try:
            _post_json("/api/presence/ping", {
                "tabId": _TAB_ID,
                "page": "/terminal",
                "title": "Yuvin Terminal " + VERSION + " — " + CURRENT_PAGE,
                "site": "classic",
            })
        except Exception:
            pass
        _STOP.wait(25)


_BEACON = None


def start_beacon():
    global _BEACON
    _BEACON = threading.Thread(target=_beacon_loop, daemon=True)
    _BEACON.start()


def goodbye():
    _STOP.set()
    try:
        # let any in-flight ping finish first, so it can't land after the
        # bye and leave a ghost tab in the Live Tabs console
        if _BEACON is not None:
            _BEACON.join(timeout=6)
        if ARGS and not ARGS.offline:
            _post_json("/api/presence/bye", {"tabId": _TAB_ID}, timeout=2)
    except (Exception, KeyboardInterrupt):
        pass  # a second Ctrl+C here just means "really quit" — so we do
    say(col(C.BLUE, "\nThanks for visiting Yuvin's Test Services. See you at " + SITE + "!"))
    sys.exit(0)

# ---------------------------------------------------------------- experiences

def age_tester():
    heading(sym("🧪 ", "") + "Age Tester")
    say("Our intelligent age classification system categorizes you into")
    say("the appropriate age group — the very first YTS experience, ever.\n")
    age = to_int(ask("How old are you? "))
    if age is None:
        say(col(C.RED, "\nThat doesn't look like a number — try the Age Tester again!"))
        return
    if age < 0:
        say(col(C.RED, "\nNegative years?! You haven't been born yet. Come back later!"))
        return
    if age > 130:
        say(col(C.YELLOW, "\nOver 130?! You might be the oldest person in history. 🏆" if EMOJI_OK
                else "\nOver 130?! You might be the oldest person in history."))
        return
    groups = [
        (9,   "Kids (0-9 years)",        "Welcome, young explorer!"),
        (12,  "Pre-Teens (10-12 years)", "The perfect age to start building things."),
        (17,  "Teens (13-17 years)",     "Big ideas start here."),
        (24,  "Post-Teens (18-24 years)", "Out in the world — keep testing!"),
        (64,  "Adults (25-64 years)",    "Plenty of experiences here for you too."),
        (130, "Seniors (65+ years)",     "Wisdom mode: activated."),
    ]
    for limit, group, msg in groups:
        if age <= limit:
            say("")
            card("Your result", [
                "Age group: " + col(C.BOLD + C.GREEN, group),
                msg,
            ])
            return


def maths_games():
    heading(sym("🔢 ", "") + "Maths Games")
    say("Sharpen your arithmetic skills. Pick your challenge:\n")
    say("  [1] Addition      [2] Subtraction")
    say("  [3] Multiplication  [4] Division\n")
    choice = ask("Choose 1-4: ")
    ops = {"1": "+", "2": "-", "3": "x", "4": "/"}
    if choice not in ops:
        say(col(C.RED, "\nPick a number between 1 and 4 next time!"))
        return
    op = ops[choice]
    score = 0
    rounds = 5
    say("")
    for i in range(1, rounds + 1):
        if op == "+":
            a, b = random.randint(1, 50), random.randint(1, 50)
            answer = a + b
        elif op == "-":
            a, b = random.randint(1, 50), random.randint(1, 50)
            if b > a:
                a, b = b, a
            answer = a - b
        elif op == "x":
            a, b = random.randint(2, 12), random.randint(2, 12)
            answer = a * b
        else:
            b, answer = random.randint(2, 12), random.randint(2, 12)
            a = b * answer
        shown_op = {"x": "×", "/": "÷"}.get(op, op) if UNICODE_OK else op
        raw = ask("Q{}/{}:  {} {} {} = ".format(i, rounds, a, shown_op, b))
        try:
            if int(raw) == answer:
                say(col(C.GREEN, "  Correct!"))
                score += 1
            else:
                say(col(C.RED, "  Not quite — it was {}.".format(answer)))
        except ValueError:
            say(col(C.RED, "  That wasn't a number — it was {}.".format(answer)))
    say("")
    stars = {5: "Perfect score! Legendary.", 4: "So close to perfect!",
             3: "Nice work — keep practicing!"}
    card("Round complete", [
        "Score: " + col(C.BOLD + C.GREEN, "{}/{}".format(score, rounds)),
        stars.get(score, "Every round makes you sharper. Go again!"),
    ])
    state = load_state()
    state["maths_rounds"] = state.get("maths_rounds", 0) + 1
    state["maths_best"] = max(state.get("maths_best", 0), score)
    save_state(state)


def mood_checker():
    heading(sym("😊 ", "") + "Mood Checker")
    say("Check in with yourself. How are you feeling right now?\n")
    moods = [
        ("Happy",  sym("😄", ":D"), "Love that! Ride the good mood and build something fun today."),
        ("Okay",   sym("🙂", ":)"), "Steady is good. A small win — like one maths round — can lift an okay day."),
        ("Tired",  sym("🥱", "-_-"), "Rest is part of the process. Even servers need downtime."),
        ("Sad",    sym("😢", ":("), "That's okay — feelings pass like weather. Be kind to yourself, and talk to someone you trust."),
        ("Angry",  sym("😤", ">:("), "Take a slow breath. Debugging feelings works like debugging code: one step at a time."),
    ]
    for i, (name, face, _) in enumerate(moods, 1):
        say("  [{}] {} {}".format(i, face, name))
    say("")
    choice = to_int(ask("Choose 1-5: "))
    if choice is not None and 1 <= choice <= 5:
        name, face, advice = moods[choice - 1]
        say("")
        card("Mood logged: " + name + " " + face, [advice,
             "Thanks for checking in with yourself today."])
    else:
        say(col(C.RED, "\nPick a number between 1 and 5 next time!"))


def personality_tester():
    heading(sym("🧠 ", "") + "Personality Tester")
    say("Discover your coding personality. 4 questions — answer honestly!\n")
    questions = [
        ("A brand new project starts. You first...",
         ["Start building immediately", "Explore every idea you could try",
          "Sketch how it should look", "Plan how you'll test it"]),
        ("Something breaks. You...",
         ["Rebuild that part better", "Try a totally different approach",
          "Check if it still looks right", "Hunt the bug until it's found"]),
        ("Your favorite part of a project is...",
         ["Watching it come alive", "Learning something new",
          "Making it beautiful", "Making it bulletproof"]),
        ("Friends describe you as...",
         ["The maker", "The adventurer", "The artist", "The detective"]),
    ]
    tally = [0, 0, 0, 0]
    for qi, (q, opts) in enumerate(questions, 1):
        say(col(C.BOLD, "Q{}. {}".format(qi, q)))
        for i, o in enumerate(opts, 1):
            say("   [{}] {}".format(i, o))
        while True:
            choice = to_int(ask("Your answer (1-4): "))
            if choice is not None and 1 <= choice <= 4:
                tally[choice - 1] += 1
                break
            say(col(C.RED, "  Please pick 1, 2, 3 or 4."))
        say("")
    results = [
        (sym("🔨 ", "") + "The Builder",  "You turn ideas into real things, fast. Whole websites start with people like you."),
        (sym("🧭 ", "") + "The Explorer", "You try what nobody else thinks of. New experiences exist because you wondered 'what if?'"),
        (sym("🎨 ", "") + "The Designer", "You make things people love to look at and use. Beauty is a feature."),
        (sym("🔍 ", "") + "The Debugger", "You find what everyone else missed. Every great project needs your sharp eyes."),
    ]
    name, desc = results[tally.index(max(tally))]
    card("Your coding personality: " + name, [desc,
         "Share your result on the forums at " + SITE + "/forum"])


def test_arena():
    heading(sym("🎯 ", "") + "Smart Test Arena " + badge("NEW"))
    say("Interactive questions, XP points, and a performance report.\n")
    pool = [
        ("What does HTML stand for?",
         ["HyperText Markup Language", "HighTech Modern Language",
          "HyperTool Making Language"], 1),
        ("Which language were the first 5 YTS scripts written in?",
         ["JavaScript", "Python", "Scratch"], 2),
        ("What symbol starts a Python comment?", ["//", "#", "<!--"], 2),
        ("12 x 12 = ?", ["124", "144", "112"], 2),
        ("Which one is a real YTS experience?",
         ["Mood Checker", "Cloud Jumper", "Pizza Builder"], 1),
        ("A 'bug' in coding is...",
         ["An insect in the computer", "A mistake in the code",
          "A type of keyboard"], 2),
        ("What year did Yuvin's Test Services begin?", ["2024", "2020", "2026"], 1),
        ("100 - 47 = ?", ["57", "53", "63"], 2),
    ]
    questions = random.sample(pool, 6)
    xp, streak, correct = 0, 0, 0
    for qi, (q, opts, right) in enumerate(questions, 1):
        say(col(C.BOLD, "Q{}. {}".format(qi, q)))
        for i, o in enumerate(opts, 1):
            say("   [{}] {}".format(i, o))
        choice = ask("Your answer: ")
        if to_int(choice) == right:
            streak += 1
            gained = 10 + (5 if streak >= 2 else 0)
            xp += gained
            correct += 1
            say(col(C.GREEN, "  Correct! +{} XP".format(gained) +
                    ("  (streak bonus!)" if streak >= 2 else "")))
        else:
            streak = 0
            say(col(C.RED, "  The answer was: " + opts[right - 1]))
        say("")
    accuracy = round(correct * 100 / len(questions))
    state = load_state()
    best = max(state.get("arena_best", 0), xp)
    state["arena_best"] = best
    state["total_xp"] = state.get("total_xp", 0) + xp
    state["arena_runs"] = state.get("arena_runs", 0) + 1
    save_state(state)
    rating = ("Legendary performance!" if accuracy >= 90 else
              "Great work!" if accuracy >= 60 else "Keep practicing — XP adds up!")
    card("Performance report", [
        "Correct answers: {}/{}   Accuracy: {}%".format(correct, len(questions), accuracy),
        "XP earned: " + col(C.BOLD + C.GREEN, str(xp)) +
        "   Personal best: " + str(best),
        rating,
    ])


def yuvinos():
    heading(sym("💻 ", "") + "YuvinOS")
    say("Booting the experimental Yuvin operating system...")
    if sys.stdout.isatty():
        try:
            for step in ("kernel", "experiences", "achievements", "sparkles"):
                say(col(C.GRAY, "  loading " + step + "..."))
                time.sleep(0.25)
        except KeyboardInterrupt:
            say("")
            goodbye()
    say(col(C.GREEN, "\nYuvinOS ready.") + " Type " + col(C.BOLD, "help") +
        " for commands, " + col(C.BOLD, "exit") + " to shut down.\n")
    jokes = [
        "Why do programmers prefer dark mode? Because light attracts bugs.",
        "There are only 10 kinds of people: those who know binary and those who don't.",
        "A bug in the hand is worth two in production.",
        "It's not a bug — it's an undocumented feature.",
        "Why did the developer go broke? Because they used up all their cache.",
    ]
    while True:
        cmd = ask("yuvinos> ").lower()
        if cmd in ("exit", "shutdown", "quit", "0"):
            say(col(C.GRAY, "Shutting down YuvinOS... goodbye!"))
            return
        elif cmd == "help":
            say("  help      show this list")
            say("  about     what is YuvinOS?")
            say("  apps      installed experiences")
            say("  time      current date and time")
            say("  joke      a random developer joke")
            say("  clear     clean the screen")
            say("  exit      shut down YuvinOS")
        elif cmd == "about":
            say("YuvinOS is the experimental operating system of Yuvin's Test")
            say("Services. The full windowed version runs in your browser at")
            say("  " + col(C.CYAN, SITE + "/yuvinOS.html"))
        elif cmd == "apps":
            say("Installed on this system:")
            say("  Age Tester, Maths Games, Mood Checker, Personality Tester,")
            say("  Smart Test Arena, Drawing Canvas, Time Machine")
            say("(exit YuvinOS and pick them from the home menu)")
        elif cmd == "time":
            say(time.strftime("It is %A, %d %B %Y — %I:%M %p"))
        elif cmd == "joke":
            say(random.choice(jokes))
        elif cmd == "clear":
            clear()
        elif cmd == "":
            continue
        else:
            say(col(C.RED, "Unknown command '" + cmd + "' — type 'help'."))


def drawing_canvas():
    heading(sym("🎨 ", "") + "Drawing Canvas")
    say("Terminal edition! Type your artwork line by line — letters, symbols,")
    say("even emoji all work. Press Enter on an empty line when you're done.")
    say(col(C.GRAY, "(up to 10 lines — try characters like  / \\ _ | ( ) o * # ~ .)") + "\n")
    rows = []
    while len(rows) < 10:
        row = ask_raw("  " + str(len(rows) + 1).rjust(2) + " | ")
        if row == "":
            break
        rows.append(row[:60])
    say("")
    if not rows:
        say("An empty canvas is very artistic... but try drawing something!")
        return
    card("Your masterpiece", rows)
    say(col(C.GREEN, "Beautiful. A true work of terminal art."))
    state = load_state()
    state["drawings"] = state.get("drawings", 0) + 1
    save_state(state)
    say(col(C.GRAY, "Want colors, brushes and a mouse? The full Drawing Canvas:"))
    say("  " + col(C.CYAN, SITE + "/drawing_canvas.html"))


def time_machine():
    heading(sym("⏰ ", "") + "Time Machine")
    say("Travel through the history of Yuvin's Test Services.\n")
    events = [
        ("Sep 2024", "The beginning",
         "Five Python scripts in a command prompt. No website, no branding."),
        ("Dec 31, 2024", "yuvin.com.au opens",
         "The site goes live — on day one it was just a single index.html."),
        ("Early 2025", "The website era",
         "The scripts grow into real web experiences, led by the Age Tester."),
        ("Jan 2026", "Accounts arrive",
         "Profiles, sign-ups and achievements come to YTS."),
        ("Mar 2026", "v.1.13.0",
         "Experiences multiply — maths games, personality testers, forums."),
        ("Jul 19, 2026", "v.2.0.0",
         "A second website is born, with Daily Word and Daily Maths."),
        ("Jul 22, 2026", "v.2.1.50",
         "HTTPS everywhere, the Server Console, and Live Tabs."),
        ("Jul 24, 2026", "v.2.2.0",
         "YTS returns to the terminal — the app you're inside right now!"),
    ]
    pipe = "│" if UNICODE_OK else "|"
    dot = "●" if UNICODE_OK else "*"
    for i, (date, title, desc) in enumerate(events):
        say("  " + col(C.BLUE + C.BOLD, dot + " " + date.ljust(14)) + col(C.BOLD, title))
        say("  " + col(C.GRAY, pipe) + "               " + desc)
        if i < len(events) - 1:
            say("  " + col(C.GRAY, pipe))
    say("")
    say(col(C.GRAY, "Screenshots of the old versions live at:"))
    say("  " + col(C.CYAN, SITE + "/time_machine.html"))


def achievements():
    heading(sym("🏆 ", "") + "My Achievements")
    state = load_state()
    if not state:
        say("Nothing here yet — play the Smart Test Arena or Maths Games")
        say("and your progress will be saved right on this computer.")
        return
    card("Saved on this computer", [
        "Total XP earned:      " + col(C.BOLD, str(state.get("total_xp", 0))),
        "Arena personal best:  " + str(state.get("arena_best", 0)) + " XP",
        "Arena runs:           " + str(state.get("arena_runs", 0)),
        "Maths rounds played:  " + str(state.get("maths_rounds", 0)) +
        "   (best score {}/5)".format(state.get("maths_best", 0)),
        "Masterpieces drawn:   " + str(state.get("drawings", 0)),
    ])
    say(col(C.GRAY, "Create a free account at " + SITE + "/signup to track"))
    say(col(C.GRAY, "achievements across the whole website too."))

# ---------------------------------------------------------------- info pages

def latest_updates():
    heading(sym("📢 ", "") + "Latest Updates — Terminal Edition")
    card(VERSION + "  (July 25, 2026)", [
        "The full /home lineup is now complete in the terminal!",
        "- YuvinOS: a tiny operating system with its own command prompt",
        "- Drawing Canvas: type-your-own terminal art, saved to achievements",
        "- Time Machine: the whole YTS story from Sept 2024 to today",
        "- New home menu with letter keys for the info pages",
    ])
    card("v.1.0.0  (July 24, 2026)", [
        "First release of Yuvin's Test Services for the terminal!",
        "- Age Tester, Maths Games, Mood Checker, Personality Tester",
        "- Smart Test Arena with XP and saved personal bests",
        "- Live server status straight from yuvin.com.au",
        "- Shows up in the developers' Live Tabs console while open",
        "- Works on macOS, Linux and Windows Command Prompt",
    ])
    say("Full website changelog: " + col(C.CYAN, SITE + "/update.html"))


def origins():
    heading(sym("🌱 ", "") + "Where It All Began")
    say("September 2024. No website, no branding — just five little Python")
    say("scripts running in a command prompt. One was called " + col(C.BOLD, "yuvin.py") + ".")
    say("")
    say("By early 2025 those scripts grew into a real website, and by 2026,")
    say("Yuvin's Test Services had experiences, accounts, forums, and even")
    say("its own server console.")
    say("")
    say("And now? YTS is back in the terminal where it was born — and this")
    say("very program is named " + col(C.BOLD, "yuvin.py") + " in honor of the original.")
    say("")
    say("The full story (with the real 2024 scripts, preserved):")
    say("  " + col(C.CYAN, SITE + "/origins.html"))
    say("  " + col(C.CYAN, SITE + "/time_machine.html"))


def about_us():
    heading(sym("ℹ️ ", "") + "About Yuvin's Test Services")
    say("Run quick, age-friendly experiments built especially for curious")
    say("coders. Playful interfaces, simple logic checks, and inclusive")
    say("experiences tailored for every generation.")
    say("")
    card("Why choose YTS?", [
        sym("🎯 ", "* ") + "Age-appropriate design, from kids to seniors",
        sym("🚀 ", "* ") + "Instant feedback on every test",
        sym("🔒 ", "* ") + "Safe & secure — your privacy matters",
        sym("💡 ", "* ") + "Completely free, forever",
    ])
    say("Website:  " + col(C.CYAN, SITE))
    say("Forums:   " + col(C.CYAN, SITE + "/forum"))
    say("Dev Program:  " + col(C.CYAN, SITE + "/dev-program"))


def live_status():
    heading(sym("📡 ", "") + "Live Server Status")
    if ARGS.offline:
        say("You're in offline mode (--offline), so no checks were made.")
        return
    say("Checking " + SITE + " ...\n")
    checks = [("Website", SITE + "/"), ("Backend API", SITE + "/api/health")]
    lines = []
    for name, url in checks:
        started = time.time()
        try:
            with _urlopen(url, timeout=5) as r:
                ms = int((time.time() - started) * 1000)
                status = getattr(r, "status", None) or r.getcode()
                ok = 200 <= status < 400
            mark = col(C.GREEN, (sym("🟢", "[UP]") + " Online")) if ok else col(C.YELLOW, "Responding oddly")
            lines.append("{}  {}  ({} ms)".format(name.ljust(12), mark, ms))
        except Exception:
            lines.append("{}  {}".format(name.ljust(12), col(C.RED, sym("🔴", "[DOWN]") + " Unreachable")))
    card("yuvin.com.au", lines)
    say(col(C.GRAY, "Tip: while this app is open, developers can see it live in"))
    say(col(C.GRAY, "the Server Console's Live Tabs list, on page /terminal."))

# ---------------------------------------------------------------- home screen

LOGO = r"""
  __   __ _____  ____
  \ \ / /|_   _|/ ___|
   \ V /   | |  \___ \
    | |    | |   ___) |
    |_|    |_|  |____/
"""


def home():
    clear()
    say(col(C.BLUE + C.BOLD, LOGO.rstrip("\n")))
    say(col(C.BOLD, "  Yuvin's Test Services") + col(C.GRAY, "  —  Terminal Edition " + VERSION))
    say(col(C.GRAY, "  Home · Experiences · About · Updates      " + SITE))
    line()
    say(col(C.BOLD, "Welcome to ") + col(C.BOLD + C.BLUE, "Yuvin's") + col(C.BOLD, " Test Services"))
    say(col(C.GRAY, "Quick, age-friendly experiments for curious coders — now right"))
    say(col(C.GRAY, "inside your terminal, where it all began in 2024."))
    say("")
    heading("EXPLORE OUR EXPERIENCES")
    say("  [1] " + sym("🧪 ", "") + "Age Tester          — find your age group")
    say("  [2] " + sym("🔢 ", "") + "Maths Games         — +  -  ×  ÷ challenges")
    say("  [3] " + sym("😊 ", "") + "Mood Checker        — check in with yourself")
    say("  [4] " + sym("🧠 ", "") + "Personality Tester  — discover your coding personality")
    say("  [5] " + sym("🎯 ", "") + "Smart Test Arena    — earn XP, beat your best")
    say("  [6] " + sym("💻 ", "") + "YuvinOS             — a tiny OS with its own commands  " + badge("NEW"))
    say("  [7] " + sym("🎨 ", "") + "Drawing Canvas      — make terminal art  " + badge("NEW"))
    say("  [8] " + sym("⏰ ", "") + "Time Machine        — the story of YTS, year by year  " + badge("NEW"))
    say("")
    heading("MORE")
    say("  [9] " + sym("📢 ", "") + "Latest Updates       [S] " + sym("📡 ", "") + "Live Server Status")
    say("  [O] " + sym("🌱 ", "") + "Origins story        [I] " + sym("ℹ️  ", "") + "About Us")
    say("  [A] " + sym("🏆 ", "") + "My Achievements      [0] Exit")
    line()
    say(col(C.GRAY, "(c) 2026 Yuvin's Test Services · All rights reserved · " + VERSION))
    say("")


PAGES = {
    "1": ("Age Tester", age_tester),
    "2": ("Maths Games", maths_games),
    "3": ("Mood Checker", mood_checker),
    "4": ("Personality Tester", personality_tester),
    "5": ("Smart Test Arena", test_arena),
    "6": ("YuvinOS", yuvinos),
    "7": ("Drawing Canvas", drawing_canvas),
    "8": ("Time Machine", time_machine),
    "9": ("Latest Updates", latest_updates),
    "s": ("Live Status", live_status),
    "o": ("Origins", origins),
    "i": ("About Us", about_us),
    "a": ("My Achievements", achievements),
}


def main():
    global ARGS, COLOR, UNICODE_OK, EMOJI_OK, CURRENT_PAGE
    # even if some fancy character slips past every gate, print() must
    # never crash the whole app over it — replace it with '?' instead
    for stream in (sys.stdout, sys.stderr):
        try:
            stream.reconfigure(errors="replace")
        except Exception:
            pass

    parser = argparse.ArgumentParser(
        prog="yuvin.py", description="Yuvin's Test Services - Terminal Edition")
    parser.add_argument("--offline", action="store_true", help="never contact yuvin.com.au")
    parser.add_argument("--no-color", action="store_true", help="disable colors")
    parser.add_argument("--ascii", action="store_true", help="disable emoji and box characters")
    parser.add_argument("--version", action="version", version="Yuvin's Test Services Terminal Edition " + VERSION)
    ARGS = parser.parse_args()

    ansi_ok = enable_ansi()
    COLOR = ansi_ok and not ARGS.no_color and os.environ.get("NO_COLOR") is None
    # Classic Windows conhost claims UTF-8 (PEP 528) but its fonts can't
    # render emoji — Windows Terminal sets WT_SESSION, conhost doesn't.
    legacy_console = (os.name == "nt" and not os.environ.get("WT_SESSION")
                      and not os.environ.get("TERM_PROGRAM"))
    UNICODE_OK = (not ARGS.ascii) and can_encode("─│┌└×÷—·●")
    EMOJI_OK = (not ARGS.ascii) and not legacy_console and can_encode("🧪😊🏆")

    if not ARGS.offline:
        start_beacon()

    while True:
        CURRENT_PAGE = "Home"
        home()
        choice = ask("Where to? ").lower()
        if choice == "0" or choice in ("exit", "quit", "q"):
            goodbye()
        if choice in PAGES:
            name, fn = PAGES[choice]
            CURRENT_PAGE = name
            clear()
            fn()
            pause()
        # anything else: just redraw home


if __name__ == "__main__":
    main()
