"""Sprawdzarka do INF.04, czerwiec 2026, część I: generator haseł.

Uruchomienie z folderu, w którym leży Twój program:
    python sprawdz_hasla.py main.py

Sprawdzarka nie wystawia punktów. Pokazuje wymagania z arkusza, których
Twój program nie spełnia, zanim zrobi to egzaminator.
"""
import contextlib
import importlib.util
import inspect
import io
import os
import re
import subprocess
import sys
from collections import Counter

ZBIORY = {
    "małe litery": ("abcdefghijklmnopqrstuvwxyz", 3),
    "wielkie litery": ("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 3),
    "polskie litery": ("ąęłńóśżźĄĘŁŃÓŚŻŹ", 2),
    "cyfry": ("0123456789", 2),
    "znaki specjalne": ("!@#$%^&*()_+", 2),
}
DLUGOSC = 12
PROBY = 10_000

wyniki = []


def wynik(ok, opis, podpowiedz=""):
    wyniki.append(ok)
    print(("  OK   " if ok else "  BŁĄD ") + opis)
    if not ok and podpowiedz:
        print("         -> " + podpowiedz)


def kategoria(znak):
    for nazwa, (zbior, _) in ZBIORY.items():
        if znak in zbior:
            return nazwa
    return None


def poprawne_haslo(tekst):
    if len(tekst) != DLUGOSC:
        return False
    licznik = Counter(kategoria(z) for z in tekst)
    return all(licznik[n] == ile for n, (_, ile) in ZBIORY.items())


def wczytaj_modul(sciezka):
    spec = importlib.util.spec_from_file_location("program_ucznia", sciezka)
    modul = importlib.util.module_from_spec(spec)
    with contextlib.redirect_stdout(io.StringIO()):
        spec.loader.exec_module(modul)
    return modul


def znajdz_funkcje(modul):
    kandydaci = []
    for nazwa, funkcja in inspect.getmembers(modul, inspect.isfunction):
        if funkcja.__module__ != modul.__name__:
            continue
        wymagane = [
            p for p in inspect.signature(funkcja).parameters.values()
            if p.default is p.empty and p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
        ]
        if not wymagane:
            kandydaci.append(funkcja)
    for funkcja in kandydaci:
        for _ in range(20):
            try:
                with contextlib.redirect_stdout(io.StringIO()):
                    zwrot = funkcja()
            except Exception:
                continue
            if isinstance(zwrot, (str, list)) and len(zwrot) == DLUGOSC:
                return funkcja
    # funkcja, która za każdym razem pada, też jest funkcją do sprawdzenia - błąd pokaże sprawdz_funkcje
    for funkcja in kandydaci:
        if re.search(r"hasl|haseł|password|generuj", funkcja.__name__, re.IGNORECASE):
            return funkcja
    return None


def sprawdz_kod(zrodlo):
    print("\n1. Kod źródłowy")
    zakazane = {
        "shuffle": "random.shuffle miesza gotowy ciąg - arkusz wymaga, żeby każdy znak trafił w losowe miejsce Twoim kodem",
        "sample(": "random.sample losuje za Ciebie cały zestaw",
        "choices(": "random.choices losuje wiele znaków naraz",
        "secrets": "moduł secrets generuje gotowe ciągi",
        "token_": "token_hex/token_urlsafe to gotowe hasło z biblioteki",
        "uuid": "uuid to gotowy identyfikator z biblioteki",
    }
    znalezione = [(k, v) for k, v in zakazane.items() if k in zrodlo]
    wynik(not znalezione, "brak bibliotecznego generowania gotowego hasła",
          "; ".join(f"'{k.rstrip('(')}': {v}" for k, v in znalezione))

    pola = {
        "nazwa funkcji / metody": r"nazwa\s+(funkcji|metody)\s*:",
        "opis funkcji / metody": r"opis\s+(funkcji|metody)\s*:",
        "parametry": r"parametry\s*:",
        "zwracany typ i opis": r"zwracany\s+typ\s+i\s+opis\s*:",
        "autor": r"autor\s*:\s*\S",
    }
    brak = [n for n, wzor in pola.items() if not re.search(wzor, zrodlo, re.IGNORECASE)]
    wynik(not brak, "nagłówek dokumentacji według listingu 1 (część III)",
          "brakuje pól: " + ", ".join(brak))


def sprawdz_funkcje(funkcja):
    print(f"\n2. Funkcja {funkcja.__name__}() - {PROBY} wywołań")
    hasla, bledy = [], []
    for _ in range(PROBY):
        try:
            hasla.append(funkcja())
        except Exception as wyjatek:
            bledy.append(f"{type(wyjatek).__name__}: {wyjatek}")
    podpowiedzi = {
        "IndexError": "indeks poza napisem: randint(a, b) losuje z b WŁĄCZNIE, więc dla zbioru bierz randint(0, len(zbior) - 1)",
        "ValueError": "randint(0, -1) na pustym haśle: przy wstawianiu w napis pozycja to randint(0, len(haslo))",
    }
    rodzaj = bledy[0].split(":")[0] if bledy else ""
    wynik(not bledy, "żadne wywołanie nie kończy się błędem",
          f"{len(bledy)} z {PROBY} wywołań padło, np. {bledy[0] if bledy else ''}. "
          + podpowiedzi.get(rodzaj, ""))
    if not hasla:
        return

    wynik(all(isinstance(h, str) for h in hasla), "zwraca napis (str), nie listę",
          f"zwrócony typ: {type(hasla[0]).__name__}")
    hasla = [h if isinstance(h, str) else "".join(map(str, h)) for h in hasla]
    wynik(all(len(h) == DLUGOSC for h in hasla), f"każde hasło ma {DLUGOSC} znaków",
          f"spotkane długości: {sorted({len(h) for h in hasla})}")

    obce = {z for h in hasla for z in h if kategoria(z) is None}
    wynik(not obce, "wszystkie znaki pochodzą ze zbiorów z arkusza",
          f"znaki spoza zbiorów: {''.join(sorted(obce))!r}")

    for nazwa, (_, ile) in ZBIORY.items():
        zle = [h for h in hasla if sum(kategoria(z) == nazwa for z in h) != ile]
        wynik(not zle, f"{nazwa}: dokładnie {ile} w każdym haśle",
              f"{len(zle)} haseł się nie zgadza, np. {zle[0]!r}" if zle else "")

    pozycje = {n: Counter() for n in ZBIORY}
    for h in hasla:
        for i, z in enumerate(h[:DLUGOSC]):
            if kategoria(z):
                pozycje[kategoria(z)][i] += 1
    stale = []
    for nazwa, (_, ile) in ZBIORY.items():
        oczekiwane = PROBY * ile / DLUGOSC
        for i in range(DLUGOSC):
            if abs(pozycje[nazwa][i] - oczekiwane) > 0.3 * oczekiwane:
                stale.append(f"{nazwa} na pozycji {i + 1}: {pozycje[nazwa][i]} razy zamiast ok. {oczekiwane:.0f}")
    wynik(not stale, "każdy rodzaj znaku trafia na każdą pozycję równie często",
          stale[0] + (f" (i {len(stale) - 1} podobnych)" if len(stale) > 1 else "")
          + ". Przy wstawianiu w napis losuj pozycję z randint(0, len(haslo)) - z końcem włącznie" if stale else "")

    rozne = len(set(hasla))
    wynik(rozne > PROBY * 0.99, "hasła się nie powtarzają", f"tylko {rozne} różnych na {PROBY}")


def sprawdz_program(sciezka):
    print("\n3. Program główny")
    srodowisko = dict(os.environ, PYTHONIOENCODING="utf-8")
    try:
        przebieg = subprocess.run(
            [sys.executable, sciezka], input="", capture_output=True,
            text=True, encoding="utf-8", timeout=10, env=srodowisko,
        )
    except subprocess.TimeoutExpired:
        wynik(False, "program kończy działanie", "po 10 sekundach nadal działa - pętla bez końca albo czeka na input()")
        return
    if przebieg.returncode != 0:
        ostatnia = (przebieg.stderr.strip().splitlines() or ["?"])[-1]
        wynik(False, "program uruchamia się bez błędu", ostatnia)
        return
    slowa = przebieg.stdout.split()
    hasla = [s for s in slowa if poprawne_haslo(s)]
    wynik(len(hasla) == 5, "wyświetla 5 haseł", f"znalezione hasła: {len(hasla)}")
    wynik(len(slowa) > len(hasla), "hasła mają znaczący komunikat", "na ekranie są same hasła, bez opisu")
    print("\n  Twój program wypisał:")
    for linia in przebieg.stdout.rstrip().splitlines():
        print("    " + linia)


def main():
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")
    sciezka = sys.argv[1] if len(sys.argv) > 1 else "main.py"
    if not os.path.isfile(sciezka):
        print(f"Nie ma pliku {sciezka}. Uruchom: python sprawdz_hasla.py <twój_plik>.py")
        sys.exit(2)
    print(f"Sprawdzam {sciezka}")
    with open(sciezka, encoding="utf-8") as plik:
        sprawdz_kod(plik.read())

    try:
        modul = wczytaj_modul(sciezka)
    except Exception as wyjatek:
        wynik(False, "plik da się wczytać", f"{type(wyjatek).__name__}: {wyjatek}")
        modul = None
    funkcja = znajdz_funkcje(modul) if modul else None
    if funkcja:
        sprawdz_funkcje(funkcja)
    else:
        print("\n2. Funkcja generująca hasło")
        wynik(False, "jest funkcja bez parametrów, która zwraca 12-znakowy napis",
              "arkusz wymaga funkcji ZWRACAJĄCEJ hasło (return), nie wypisującej go")

    sprawdz_program(sciezka)
    zdane = sum(wyniki)
    print(f"\nWynik: {zdane}/{len(wyniki)} wymagań spełnionych")
    sys.exit(0 if zdane == len(wyniki) else 1)


if __name__ == "__main__":
    main()
