"""Sprawdzarka do INF.04, czerwiec 2025, część I: loteria.

Uruchomienie z folderu, w którym leży Twój program:
    python sprawdz_loteria.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 builtins
import contextlib
import importlib.util
import inspect
import io
import os
import re
import subprocess
import sys

LOSOWANIE = re.compile(r"^\s*\D*?(\d+)\s*:\s*((?:\d+[\s,;]*){1,})$")
WYSTAPIENIA = re.compile(r"(\d+)\D+?(\d+)\s*$")

wyniki = []


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


def uruchom(sciezka, wejscie):
    srodowisko = dict(os.environ, PYTHONIOENCODING="utf-8")
    return subprocess.run([sys.executable, sciezka], input=wejscie, capture_output=True,
                          text=True, encoding="utf-8", timeout=20, env=srodowisko)


def rozbierz(wypis):
    """Zwraca (lista zestawów, słownik liczba -> wystąpienia) odczytane z ekranu."""
    zestawy, wystapienia = [], {}
    for linia in wypis.splitlines():
        if re.search(r"wyst", linia, re.IGNORECASE):
            m = WYSTAPIENIA.search(linia)
            if m:
                wystapienia[int(m.group(1))] = int(m.group(2))
        elif re.search(r"losowan|zestaw", linia, re.IGNORECASE):
            m = LOSOWANIE.match(linia)
            if m:
                zestawy.append([int(x) for x in re.findall(r"\d+", m.group(2))])
    return zestawy, wystapienia


def wczytaj_modul(sciezka):
    spec = importlib.util.spec_from_file_location("program_ucznia", sciezka)
    modul = importlib.util.module_from_spec(spec)
    oryginalny_input = builtins.input
    builtins.input = lambda *a: "3"
    try:
        with contextlib.redirect_stdout(io.StringIO()):
            spec.loader.exec_module(modul)
    finally:
        builtins.input = oryginalny_input
    return modul


def sprawdz_kod(zrodlo):
    print("\n1. Kod źródłowy")
    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))
    wynik(not re.search(r"^\s*global\s", zrodlo, re.MULTILINE), "bez słowa global",
          "funkcja ma dostać dane parametrem i oddać wynik przez return")
    wynik("sample(" not in zrodlo and "shuffle(" not in zrodlo, "losowanie bez powtórzeń zrobione samodzielnie",
          "random.sample / shuffle robią to za Ciebie; kryterium 2.6 ocenia, czy program IMPLEMENTUJE losowanie bez powtórzeń")


def sprawdz_funkcje(modul):
    print("\n2. Funkcje")
    funkcje = {n: f for n, f in inspect.getmembers(modul, inspect.isfunction) if f.__module__ == modul.__name__}
    poza_main = [n for n in funkcje if n != "main"]
    wynik(len(poza_main) >= 2, f"co najmniej dwie funkcje poza programem głównym ({', '.join(poza_main) or 'brak'})",
          "arkusz: funkcja wypełniająca kolekcję i funkcja wyświetlająca losowania")

    globalne = [n for n, w in vars(modul).items()
                if not n.startswith("_") and not n.isupper()
                and not inspect.ismodule(w) and not inspect.isfunction(w) and not inspect.isclass(w)]
    wynik(not globalne, "brak zmiennych globalnych", f"na poziomie pliku leżą zmienne: {', '.join(globalne)}. "
          "Kod programu głównego wstaw do def main(): i wywołaj main() na dole pliku")

    wypelniajaca = None
    for nazwa, funkcja in funkcje.items():
        parametry = [p for p in inspect.signature(funkcja).parameters.values() if p.default is p.empty]
        if len(parametry) != 1:
            continue
        try:
            with contextlib.redirect_stdout(io.StringIO()):
                zwrot = funkcja(5)
        except Exception:
            continue
        if isinstance(zwrot, (list, tuple)) and len(zwrot) == 5 and all(isinstance(z, (list, tuple, set)) for z in zwrot):
            wypelniajaca = (nazwa, funkcja)
            break
    wynik(wypelniajaca, "funkcja losująca zwraca kolekcję n zestawów" + (f": {wypelniajaca[0]}()" if wypelniajaca else ""),
          "nie znalazłem funkcji f(n), która zwraca listę n zestawów. Dane muszą przetrwać wyjście z funkcji: return zestawy")
    if not wypelniajaca:
        return
    nazwa, funkcja = wypelniajaca
    with contextlib.redirect_stdout(io.StringIO()):
        zestawy = [z for _ in range(200) for z in funkcja(10)]
        pusty = funkcja(0)
    wynik(all(len(z) == 6 for z in zestawy), "każdy zestaw ma 6 liczb", f"spotkane długości: {sorted({len(z) for z in zestawy})}")
    wynik(all(len(set(z)) == len(z) for z in zestawy), "liczby w zestawie się nie powtarzają",
          f"np. {next((list(z) for z in zestawy if len(set(z)) != len(z)), '')}. Losuj, dopóki zestaw nie ma 6 RÓŻNYCH liczb")
    liczby = [x for z in zestawy for x in z]
    wynik(min(liczby) >= 1 and max(liczby) <= 49, "liczby z zakresu 1-49", f"minimum {min(liczby)}, maksimum {max(liczby)}")
    wynik(1 in liczby and 49 in liczby, "losowanie obejmuje 1 i 49",
          "w 12 000 liczbach nie wypadło 1 albo 49: random.randint(1, 49) losuje z oboma końcami")
    wynik(len(pusty) == 0, f"{nazwa}(0) zwraca pustą kolekcję", f"zwraca {len(pusty)} zestawów - pętla ma iść po rzeczywistej liczbie losowań")


def sprawdz_program(sciezka):
    print("\n3. Program główny")
    for ile in (11, 1):
        try:
            przebieg = uruchom(sciezka, f"{ile}\n")
        except subprocess.TimeoutExpired:
            wynik(False, f"program kończy działanie dla {ile}", "po 20 sekundach nadal działa")
            return
        if przebieg.returncode != 0:
            wynik(False, f"program dla {ile} uruchamia się bez błędu", (przebieg.stderr.strip().splitlines() or ["?"])[-1])
            return
        zestawy, wystapienia = rozbierz(przebieg.stdout)
        wynik(len(zestawy) == ile, f"dla {ile} wyświetla {ile} zestaw{'ów' if ile > 1 else ''}",
              f"rozpoznanych linii z zestawami: {len(zestawy)}. Format jak na obrazie 1: „Losowanie 1: 18 31 49 4 47 11”")
        policzone = {n: sum(z.count(n) for z in zestawy) for n in range(1, 50)}
        zle = [n for n in range(1, 50) if wystapienia.get(n, 0) != policzone[n]]
        brakujace_niezerowe = [n for n in range(1, 50) if policzone[n] and n not in wystapienia]
        wynik(wystapienia and not zle and not brakujace_niezerowe, f"dla {ile}: wystąpienia liczb 1-49 zgadzają się z zestawami",
              f"niezgodne liczby: {zle[:5]}" if wystapienia else "brak linii „Wystąpienia liczby 1: 1”")
        if ile == 11:
            pierwsza = przebieg.stdout.split("\n", 1)[0]
            wynik(re.search(r"[a-ząćęłńóśźż]{3}", pierwsza, re.IGNORECASE), "program pyta o liczbę losowań zrozumiałym komunikatem",
                  "input() bez tekstu: użytkownik widzi pusty ekran")
            print("\n  Twój program wypisał (początek):")
            for linia in przebieg.stdout.splitlines()[:6]:
                print("    " + linia)
            print()


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_loteria.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
    if modul:
        sprawdz_funkcje(modul)
    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()
