from dataclasses import dataclass
from math import isfinite
from typing import Protocol
import unittest
import io

@dataclass(frozen=True)
class Meting:
    waarde: float
    tijd_s: float
    eenheid: str = "degC"

class Sensor(Protocol):
    def lees(self) -> Meting: ...

class ProefSensor:
    def __init__(self, waarde, tijd_s):
        self.waarde, self.tijd_s = waarde, tijd_s
    def lees(self):
        return Meting(self.waarde, self.tijd_s)

class Regelaar:
    def __init__(self, sensor, kp=200.0, grens=2000.0, max_leeftijd=5.0):
        if (not all(isfinite(x) for x in (kp, grens, max_leeftijd))
                or kp < 0 or grens <= 0 or max_leeftijd <= 0):
            raise ValueError("ongeldige instelling")
        self.sensor = sensor             # samenstelling, geen Sensor-subklasse
        self.kp, self.grens = kp, grens
        self.max_leeftijd = max_leeftijd
    def stap(self, setpoint, nu):
        if not isfinite(setpoint) or not -30 <= setpoint <= 60 or not isfinite(nu):
            raise ValueError("ongeldig setpoint of tijdstip")
        m = self.sensor.lees()
        if (m.eenheid != "degC" or not isfinite(m.waarde) or not isfinite(m.tijd_s)
                or not -30 <= m.waarde <= 60
                or not 0 <= nu - m.tijd_s <= self.max_leeftijd):
            raise ValueError("meting onbruikbaar; supervisor kiest veilige modus")
        return min(self.grens, max(0.0, self.kp * (setpoint - m.waarde)))

class TestRegelaar(unittest.TestCase):
    def test_normaal(self):
        self.assertEqual(Regelaar(ProefSensor(18,10)).stap(20,11), 400)
    def test_begrenzing(self):
        self.assertEqual(Regelaar(ProefSensor(0,10)).stap(20,11), 2000)
        self.assertEqual(Regelaar(ProefSensor(23,10)).stap(20,11), 0)
    def test_oud(self):
        with self.assertRaises(ValueError):
            Regelaar(ProefSensor(18,10)).stap(20,16)
    def test_nan(self):
        with self.assertRaises(ValueError):
            Regelaar(ProefSensor(float("nan"),10)).stap(20,11)
    def test_eenheid(self):
        class Kelvin:
            def lees(self): return Meting(291.15,10,"K")
        with self.assertRaises(ValueError):
            Regelaar(Kelvin()).stap(20,11)
    def test_instellingen(self):
        for naam in ("kp", "grens", "max_leeftijd"):
            for waarde in (float("nan"), float("inf"), -float("inf")):
                with self.subTest(naam=naam, waarde=waarde), self.assertRaises(ValueError):
                    Regelaar(ProefSensor(18,10), **{naam:waarde})
    def test_invoer(self):
        for r,nu in [(float("nan"),11),(float("inf"),11),(20,float("nan")),(61,11)]:
            with self.subTest(r=r,nu=nu), self.assertRaises(ValueError):
                Regelaar(ProefSensor(18,10)).stap(r,nu)

log = io.StringIO()
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestRegelaar)
resultaat = unittest.TextTestRunner(stream=log).run(suite)
print("tests", resultaat.testsRun, "fouten", len(resultaat.errors),
      "mislukt", len(resultaat.failures))
print("warmtevraag", Regelaar(ProefSensor(18,10)).stap(20,11), "W")
assert resultaat.wasSuccessful()
