"""Sprawdzarka do INF.04, styczeń 2025, część I: klasa z tablicą.

Uruchomienie z folderu, w którym leży Twój program:
    python sprawdz_tablice.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

# tablica testowa: liczba elementów 10, a za nimi dwa elementy, których metody NIE mogą widzieć
WIDOCZNE = [7, 4, 7, 10, 3, 8, 1000, 1, 2, 5]
UKRYTE = [999, 999]
NIEPARZYSTE = [x for x in WIDOCZNE if x % 2]
SREDNIA = sum(WIDOCZNE) / len(WIDOCZNE)
LINIA_ELEMENTU = re.compile(r"^\s*(\d+)\s*:\s*(-?\d+)\s*$")

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 wywolaj(funkcja, *argumenty):
    bufor = io.StringIO()
    with contextlib.redirect_stdout(bufor):
        zwrot = funkcja(*argumenty)
    return zwrot, bufor.getvalue()


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: "500"
    try:
        with contextlib.redirect_stdout(io.StringIO()):
            spec.loader.exec_module(modul)
    finally:
        builtins.input = oryginalny_input
    return modul


def znajdz_klase(modul):
    for nazwa, klasa in inspect.getmembers(modul, inspect.isclass):
        if klasa.__module__ != modul.__name__:
            continue
        parametry = list(inspect.signature(klasa.__init__).parameters.values())[1:]
        wymagane = [p for p in parametry if p.default is p.empty]
        if len(wymagane) == 1:
            return klasa
    return None


def pola_obiektu(obiekt, klasa):
    """Zwraca (nazwa pola z tablicą, nazwa pola z liczbą elementów) albo None."""
    pole_tablicy = pole_liczby = None
    for nazwa, wartosc in vars(obiekt).items():
        if isinstance(wartosc, list) and pole_tablicy is None:
            pole_tablicy = nazwa
        elif isinstance(wartosc, int) and not isinstance(wartosc, bool) and pole_liczby is None:
            pole_liczby = nazwa
    return pole_tablicy, pole_liczby


def sprawdz_kod(zrodlo):
    print("\n1. Kod źródłowy")
    pola = {
        "nazwa metody": r"nazwa\s+(metody|funkcji)\s*:",
        "opis metody": r"opis\s+(metody|funkcji)\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("class " in zrodlo, "tablica i operacje są w klasie", "arkusz wymaga klasy: class Tablica:")


def sprawdz_klase(klasa):
    print(f"\n2. Klasa {klasa.__name__} - pola i konstruktor")
    try:
        obiekt = klasa(30)
    except Exception as wyjatek:
        wynik(False, "konstruktor przyjmuje rozmiar tablicy", f"{type(wyjatek).__name__}: {wyjatek}")
        return None
    pole_tablicy, pole_liczby = pola_obiektu(obiekt, klasa)
    wynik(pole_tablicy is not None, "pole z tablicą (w Pythonie lista)",
          "w konstruktorze brakuje self.__tablica = [] wypełnionej liczbami")
    wynik(pole_liczby is not None, "pole z liczbą elementów (int)",
          "w konstruktorze brakuje self.__liczba_elementow = rozmiar")
    if pole_tablicy is None or pole_liczby is None:
        return None

    prywatny = f"_{klasa.__name__}__"
    for opis, pole in (("tablica", pole_tablicy), ("liczba elementów", pole_liczby)):
        wynik(pole.startswith(prywatny), f"pole „{opis}” niedostępne poza klasą i w klasach potomnych",
              f"pole nazywa się {pole.replace(prywatny, '__')!r}. Dwa podkreślenia na początku (self.__nazwa) "
              "ukrywają pole także przed klasą potomną; jedno podkreślenie to tylko umowa i nic nie blokuje")

    tablica = getattr(obiekt, pole_tablicy)
    wynik(getattr(obiekt, pole_liczby) == 30, "konstruktor ustawia liczbę elementów na wartość argumentu",
          f"dla klasa(30) pole ma wartość {getattr(obiekt, pole_liczby)}")
    wynik(len(tablica) == 30, "konstruktor wypełnia tyle elementów, ile wynosi rozmiar",
          f"dla klasa(30) tablica ma {len(tablica)} elementów")

    wartosci = []
    for _ in range(100):
        wartosci += getattr(klasa(200), pole_tablicy)
    wynik(all(isinstance(x, int) and not isinstance(x, bool) for x in wartosci),
          "elementy są liczbami całkowitymi", "random.randint zwraca int; random.random albo uniform dają float")
    liczby = [x for x in wartosci if isinstance(x, (int, float))]
    wynik(liczby and 1 <= min(liczby) and max(liczby) <= 1000, "wartości mieszczą się w zakresie 1-1000",
          f"spotkane minimum {min(liczby) if liczby else '-'}, maksimum {max(liczby) if liczby else '-'}")
    wynik(1 in liczby and 1000 in liczby, "losowanie obejmuje oba końce: 1 i 1000",
          "w 20 000 losowań nie wypadło 1 albo 1000. random.randint(1, 1000) losuje z oboma końcami, "
          "random.randrange(1, 1000) kończy się na 999")
    wynik(len(set(liczby)) > 900, "liczby są pseudolosowe, a nie stałe", f"tylko {len(set(liczby))} różnych wartości")
    return pole_tablicy, pole_liczby


def podstaw_tablice(klasa, pola):
    pole_tablicy, pole_liczby = pola
    obiekt = klasa(30)
    setattr(obiekt, pole_tablicy, WIDOCZNE + UKRYTE)
    setattr(obiekt, pole_liczby, len(WIDOCZNE))
    return obiekt


def rozpoznaj_metody(klasa, pola):
    metody = {}
    for nazwa, metoda in inspect.getmembers(klasa, inspect.isfunction):
        if nazwa.startswith("_"):
            continue
        parametry = list(inspect.signature(metoda).parameters.values())[1:]
        wymagane = [p for p in parametry if p.default is p.empty]
        obiekt = podstaw_tablice(klasa, pola)
        try:
            if len(wymagane) == 1:
                metody.setdefault("szukaj", nazwa)
                continue
            if wymagane:
                continue
            zwrot, wypis = wywolaj(getattr(obiekt, nazwa))
        except Exception:
            continue
        if zwrot is None and wypis.strip():
            metody.setdefault("wyswietl", nazwa)
        elif isinstance(zwrot, float) or (isinstance(zwrot, int) and not isinstance(zwrot, bool) and not wypis.strip()):
            metody.setdefault("srednia", nazwa)
        elif isinstance(zwrot, int) and not isinstance(zwrot, bool):
            metody.setdefault("nieparzyste", nazwa)
    return metody


def sprawdz_metody(klasa, pola):
    print(f"\n3. Metody na podstawionej tablicy {WIDOCZNE}, liczba elementów {len(WIDOCZNE)}")
    print(f"   (za nimi w liście leżą jeszcze {UKRYTE}, których metody nie mogą widzieć)")
    metody = rozpoznaj_metody(klasa, pola)
    opisy = {
        "wyswietl": "metoda wyświetlająca elementy (bez parametrów, nic nie zwraca)",
        "szukaj": "metoda wyszukująca (jeden parametr: szukana wartość)",
        "nieparzyste": "metoda wyświetlająca nieparzyste i zwracająca ich liczbę",
        "srednia": "metoda zwracająca średnią",
    }
    for klucz, opis in opisy.items():
        wynik(klucz in metody, opis + (f": {metody[klucz]}()" if klucz in metody else ""),
              "nie znalazłem publicznej metody, która tak się zachowuje. Nazwa bez podkreślenia na początku = dostępna poza klasą")

    if "wyswietl" in metody:
        zwrot, wypis = wywolaj(getattr(podstaw_tablice(klasa, pola), metody["wyswietl"]))
        oczekiwane = [f"{i}: {x}" for i, x in enumerate(WIDOCZNE)]
        linie = [l.strip() for l in wypis.strip().splitlines() if re.search(r"\d", l)]
        wynik(linie == oczekiwane, "wyświetla dokładnie „<indeks>: <wartość>” dla elementów 0-9",
              f"pierwsze linie: {linie[:3]}, linii: {len(linie)}. Pętla po range(self.__liczba_elementow), nie po len(listy)")

    if "szukaj" in metody:
        obiekt = podstaw_tablice(klasa, pola)
        szukaj = getattr(obiekt, metody["szukaj"])
        try:
            przypadki = [(7, 0, "7 stoi pod indeksami 0 i 2 - ma wyjść pierwsze wystąpienie, czyli return w środku pętli"),
                         (1000, 6, ""),
                         (999, -1, "999 leży w liście za liczbą elementów - wyszukiwanie ma się zatrzymać na indeksie 9"),
                         (12345, -1, "brak wartości ma zwrócić -1, a nie None ani False")]
            for wartosc, oczekiwany, podpowiedz in przypadki:
                zwrot, _ = wywolaj(szukaj, wartosc)
                wynik(zwrot == oczekiwany and not isinstance(zwrot, bool), f"szukaj({wartosc}) zwraca {oczekiwany}",
                      f"zwraca {zwrot!r}. " + podpowiedz)
        except Exception as wyjatek:
            wynik(False, "wyszukiwanie działa bez błędu", f"{type(wyjatek).__name__}: {wyjatek}")

    if "nieparzyste" in metody:
        zwrot, wypis = wywolaj(getattr(podstaw_tablice(klasa, pola), metody["nieparzyste"]))
        liczby = [int(x) for x in re.findall(r"-?\d+", wypis)]
        wynik(zwrot == len(NIEPARZYSTE), f"zwraca liczbę nieparzystych: {len(NIEPARZYSTE)}",
              f"zwraca {zwrot!r}" + (" - policzyła też 999 spoza liczby elementów" if zwrot == len(NIEPARZYSTE) + 2 else ""))
        wynik(all(x in liczby for x in NIEPARZYSTE) and not any(x % 2 == 0 for x in liczby if x != 0),
              "wyświetla wszystkie nieparzyste i żadnej parzystej", f"wyświetliła: {liczby}")
        wynik(999 not in liczby, "nie wyświetla elementów spoza liczby elementów", "na ekranie jest 999")

    if "srednia" in metody:
        zwrot, _ = wywolaj(getattr(podstaw_tablice(klasa, pola), metody["srednia"]))
        podpowiedz = f"zwraca {zwrot!r}"
        if zwrot == sum(WIDOCZNE) // len(WIDOCZNE):
            podpowiedz += ". Operator // obcina część ułamkową - dziel przez /"
        elif isinstance(zwrot, (int, float)) and abs(zwrot - sum(WIDOCZNE + UKRYTE) / 12) < 0.01:
            podpowiedz += ". Policzona z całej listy - dziel przez self.__liczba_elementow, nie len(listy)"
        wynik(isinstance(zwrot, (int, float)) and abs(zwrot - SREDNIA) < 0.01, f"zwraca średnią {SREDNIA}", podpowiedz)


def sprawdz_program(sciezka):
    print("\n4. 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
              + (" - program czeka na input(), a obraz 1 pokazuje działanie bez pytań" if "EOF" in ostatnia else ""))
        return
    linie = przebieg.stdout.splitlines()
    elementy = [LINIA_ELEMENTU.match(l) for l in linie]
    tablica = [int(m.group(2)) for m in elementy if m]
    wynik(len(tablica) > 20, "obiekt ma tablicę większą niż 20 elementów", f"wyświetlonych elementów: {len(tablica)}")
    reszta = "\n".join(l for l, m in zip(linie, elementy) if not m)
    wynik(not re.search(r"(?<![\d.])-1(?![\d.])", reszta), "gdy wartości nie znaleziono, program nic nie wyświetla",
          "na ekranie jest -1. Program główny sprawdza if indeks != -1 i dopiero wtedy wypisuje komunikat")
    if tablica:
        ile = sum(1 for x in tablica if x % 2)
        wynik(re.search(rf"nieparzyst\w*\D*{ile}\b", reszta, re.IGNORECASE) is not None,
              f"liczba nieparzystych ({ile}) wyświetlona z komentarzem", "brak linii w stylu „Razem nieparzystych: 25”")
        srednia = sum(tablica) / len(tablica)
        liczby = [float(x.replace(",", ".")) for x in re.findall(r"\d+(?:[.,]\d+)?", reszta)]
        wynik(re.search(r"średni", reszta, re.IGNORECASE) and any(abs(x - srednia) < 1 for x in liczby),
              f"średnia ({srednia:.2f}) wyświetlona z komentarzem", "brak linii w stylu „Średnia wszystkich elementów: 525.34”")
    print("\n  Twój program wypisał (początek i koniec):")
    pokaz = linie if len(linie) <= 12 else linie[:4] + ["    ..."] + linie[-8:]
    for linia in pokaz:
        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_tablice.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
    klasa = znajdz_klase(modul) if modul else None
    if klasa is None:
        print("\n2. Klasa")
        wynik(False, "jest klasa, której konstruktor przyjmuje rozmiar tablicy",
              "def __init__(self, rozmiar): - jeden parametr poza self")
    else:
        pola = sprawdz_klase(klasa)
        if pola:
            sprawdz_metody(klasa, pola)

    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()
