﻿"""
Zadanie 05: Walidacja biznesowa w systemie bankowym

Zadanie stanowi podsumowanie nabytej wiedzy o wyjątkach. Prezentuje pełną hierarchię
autorskich klas wyjątków dla logiki bankowej (BankError -> InvalidAmountError, itd.).
Pokazuje również zastosowanie instrukcji assert do wykrywania stanów niemożliwych
według logiki, a także prezentuje rygorystyczną pętlę testową sprawdzającą odporność.
"""

# ======================= HIERARCHIA WYJĄTKÓW =======================

class BankError(Exception):
    """
    Bazowa klasa dla wszystkich błędów związanych z modułem bankowym.
    Pozwala na łatwe przechwycenie każdego błędu z tej domeny w jednym bloku except BankError,
    jeśli nie zależy nam na detalach, lub rozróżnianie podtypów, jeśli zależy.
    """
    pass

class InvalidAmountError(BankError):
    """Błąd rzucany, gdy kwota operacji (np. przelewu) jest niewłaściwa (ujemna lub zero)."""
    pass

class InsufficientFundsError(BankError):
    """Błąd rzucany w przypadku próby wykonania operacji przewyższającej saldo."""
    def __init__(self, message: str, brakujaca_kwota: float):
        super().__init__(message)
        self.brakujaca_kwota = brakujaca_kwota

class AccountLockedError(BankError):
    """Błąd rzucany, gdy konto jest zablokowane i nie można na nim wykonywać operacji."""
    pass


# ======================= KLASA DOMENOWA =======================

class KontoBankowe:
    def __init__(self, wlasciciel: str, saldo_poczatkowe: float):
        self.wlasciciel = wlasciciel
        self.saldo = saldo_poczatkowe
        self.czy_aktywne = True
        
    def zablokuj_konto(self):
        """Zmienia status konta na zablokowane."""
        self.czy_aktywne = False
        
    def przelew(self, kwota: float, cel: str) -> None:
        """
        Zleca przelew określonej kwoty na inne konto.
        Rygorystycznie weryfikuje stany przed operacją, rzucając dedykowane wyjątki biznesowe.
        
        Args:
            kwota (float): Wartość przelewu.
            cel (str): Nazwa lub numer konta docelowego.
            
        Raises:
            AccountLockedError: Gdy konto jest nieaktywne.
            InvalidAmountError: Gdy podano kwotę ujemną lub zero.
            InsufficientFundsError: Gdy brakuje środków do realizacji przelewu.
            TypeError: Gdy kwota nie jest liczbą.
        """
        # 1. Sprawdzenie typu danych wejściowych (np. czy kwota nie jest stringiem)
        if not isinstance(kwota, (int, float)):
            # Logowanie wewnętrzne błędu przed jego wyrzuceniem
            print(f"[LOG WEWNĘTRZNY] Atak lub błąd typów: przekazano {type(kwota).__name__}")
            raise TypeError("Kwota przelewu musi być liczbą.")
            
        # 2. Sprawdzenie czy konto nie jest zablokowane
        if not self.czy_aktywne:
            print(f"[LOG AUDYTOWY] Próba operacji z zablokowanego konta użytkownika {self.wlasciciel}")
            raise AccountLockedError("Nie można wykonać operacji: Konto jest zablokowane ze względów bezpieczeństwa.")
            
        # 3. Sprawdzenie poprawności kwoty (nie może być ujemna ani równa zero)
        if kwota <= 0:
            print(f"[LOG WEWNĘTRZNY] Próba zlecenia operacji na niedozwoloną kwotę: {kwota}")
            raise InvalidAmountError("Kwota przelewu musi być wartością dodatnią.")
            
        # 4. Sprawdzenie czy starczy środków
        if kwota > self.saldo:
            brak = kwota - self.saldo
            print(f"[LOG WEWNĘTRZNY] Odrzucenie przelewu na rzecz {cel} z powodu braku środków.")
            raise InsufficientFundsError(f"Odmowa zlecenia. Brak środków.", brak)
            
        # Sprawdzanie niezmienników (invariantów) za pomocą assert.
        # W tym miejscu saldo i kwota są zwalidowane, więc logicznie jest niemożliwe,
        # by po operacji saldo stało się ujemne. Assert służy do chwytania błędów LOGICZNYCH programisty.
        assert self.saldo - kwota >= 0, "Błąd krytyczny logiki aplikacji: saldo stałoby się ujemne po operacji"
            
        # Operacja właściwa
        self.saldo -= kwota
        print(f"[SUKCES] Wykonano przelew w wysokości {kwota} na konto: {cel}.")


# ======================= PĘTLA TESTOWA =======================

if __name__ == "__main__":
    konto = KontoBankowe("Jan Kowalski", 200.0)
    
    # Lista przypadków testowych symulujących różne, często skrajne użycia
    transakcje_testowe = [
        (50.0, "ZUS"),                # Powinno przejść (zostanie 150)
        (1000.0, "Urząd Skarbowy"),   # Brak środków (brak: 850)
        (-50.0, "Sklep ABC"),         # Ujemna kwota
        ("sto", "Znajomy"),           # Błąd typu (tekst zamiast liczby)
        (10.0, "Opłata po blokadzie") # Test blokady
    ]
    
    print("--- ROZPOCZĘCIE TESTÓW ODPORNOŚCIOWYCH ---")
    
    for kwota, cel in transakcje_testowe:
        # Celowo blokujemy konto przed ostatnią transakcją w celach testowych
        if cel == "Opłata po blokadzie":
            konto.zablokuj_konto()
            
        print(f"\n>>> Próba przelewu {kwota} na cel: {cel}...")
        
        try:
            konto.przelew(kwota, cel)
            
        # Chwytamy wyjątki od najbardziej szczegółowych do ogólnych
        except InsufficientFundsError as e:
            print(f"[OPERACJA ODRZUCONA] Brak wystarczających środków. Dostępne: {konto.saldo} zł. Brakująca kwota: {e.brakujaca_kwota} zł.")
            
        except InvalidAmountError as e:
            print(f"[BŁĄD WALIDACJI] Kwota przelewu nie może być ujemna lub zerowa! ({e})")
            
        except AccountLockedError as e:
            print(f"[BLOKADA BEZPIECZEŃSTWA] Dostęp do konta zablokowany: {e}")
            
        except TypeError as e:
            print(f"[BŁĄD SYSTEMOWY] Przekazano nieprawidłowy format danych: {e}")
            
        except BankError as e:
            # Gdyby pojawił się inny błąd biznesowy z naszej domeny, wpadnie tutaj.
            print(f"[BŁĄD BANKOWY] Wystąpił inny błąd biznesowy: {e}")
            
        except Exception as e:
            # Najbardziej ogólne przechwycenie na samym dole - połykanie Exception: pass
            # to najgorsza praktyka. Zawsze chwytamy i logujemy niespodziewane awarie.
            print(f"[AWARIA KRYTYCZNA] Skontaktuj się z administratorem: {e}")
            
    print("\n--- ZAKOŃCZENIE TESTÓW ---")
    print(f"Końcowe saldo konta: {konto.saldo} zł")
