"""Verfügbarkeit: Circuit-Breaker, Claude-Ausfall, Codex-Ausfall, Timeout, kein ungeprüfter Fallback."""
import tempfile
import unittest
from pathlib import Path

from aria_router.gateway import Handling
from aria_router.health import HealthRegistry
from aria_router.runner import MockRunner, RunResult

from tests.helpers import FakeClock, GatewayTestCase, RecordingSink


class CircuitBreakerTests(unittest.TestCase):
    def setUp(self):
        self._tmp = tempfile.TemporaryDirectory()
        self.path = Path(self._tmp.name) / "health.json"
        self.clock = FakeClock()
        self.registry = HealthRegistry(self.path, 3, 300, clock=self.clock)

    def tearDown(self):
        self._tmp.cleanup()

    def test_opens_after_threshold(self):
        for _ in range(2):
            self.registry.record_failure("codex", "TIMEOUT")
        self.assertTrue(self.registry.is_available("codex"))
        self.registry.record_failure("codex", "TIMEOUT")
        self.assertEqual(self.registry.state("codex").status, "open")
        self.assertFalse(self.registry.is_available("codex"))

    def test_half_open_after_open_seconds(self):
        for _ in range(3):
            self.registry.record_failure("codex", "CRASHED")
        self.clock.advance(301)
        self.assertEqual(self.registry.state("codex").status, "half_open")
        self.assertTrue(self.registry.is_available("codex"))
        self.registry.record_failure("codex", "CRASHED")
        self.assertEqual(self.registry.state("codex").status, "open")
        self.clock.advance(301)
        self.registry.record_success("codex")
        self.assertEqual(self.registry.state("codex").status, "closed")

    def test_success_resets_counter(self):
        for _ in range(2):
            self.registry.record_failure("codex", "TIMEOUT")
        self.registry.record_success("codex")
        for _ in range(2):
            self.registry.record_failure("codex", "TIMEOUT")
        self.assertEqual(self.registry.state("codex").status, "closed")

    def test_non_health_codes_ignored(self):
        for code in ("REPLAY", "INVALID_SIGNATURE", "PRIVACY_NOT_PERMITTED"):
            self.assertFalse(self.registry.record_failure("codex", code))
        self.assertEqual(self.registry.state("codex").failures, 0)

    def test_state_persisted(self):
        for _ in range(3):
            self.registry.record_failure("claude", "DELIVERY_FAILED")
        self.assertFalse(HealthRegistry(self.path, 3, 300, clock=self.clock).is_available("claude"))


class AvailabilityTests(GatewayTestCase):
    def test_codex_outage_opens_circuit_and_reroutes(self):
        runner = MockRunner(lambda spec: RunResult(False, "START_FAILED"))
        gateway = self.gateway(codex_runner=runner)
        for message_id in (1, 2, 3):
            result = gateway.handle(self.request(message_id, "#public frage"))
            self.assertEqual((result.handling, result.code), (Handling.FAILED, "START_FAILED"))
            self.assertIn("kein", result.reply_text.lower())
        self.assertEqual(gateway.status()["backends"]["codex"]["health"], "open")
        rerouted = gateway.handle(self.request(4, "#public frage"))
        self.assertEqual(rerouted.backend, "claude")
        manual = gateway.handle(self.request(5, "/codex #public frage"))
        self.assertEqual(manual.code, "MANUAL_CHOICE_NOT_PERMITTED")
        self.assertIn("nicht verfügbar", manual.reply_text)
        self.assertEqual(len(runner.calls), 3)

    def test_claude_outage_without_unchecked_fallback(self):
        sink = RecordingSink(fail=True)
        gateway = self.gateway(forward_sink=sink)
        for message_id in (6, 7, 8):
            result = gateway.handle(self.request(message_id, "hallo"))
            self.assertEqual((result.handling, result.code), (Handling.FAILED, "DELIVERY_FAILED"))
        self.assertEqual(gateway.status()["backends"]["claude"]["health"], "open")
        blocked = gateway.handle(self.request(9, "hallo"))
        # UNKNOWN darf nicht an Codex: kein Ausweichen ohne Prüfung.
        self.assertEqual((blocked.handling, blocked.code), (Handling.BLOCKED, "NO_ELIGIBLE_BACKEND"))
        self.assertEqual(gateway.adapters["codex"].runner.calls, [])

    def test_claude_delivery_report_counts(self):
        gateway = self.gateway(forward_sink=None)
        for message_id in (10, 11, 12):
            result = gateway.handle(self.request(message_id, "hallo"))
            self.assertEqual(result.handling, Handling.FORWARD_TO_CLAUDE_CHANNEL)
            nonce = result.forward["clearance"]["nonce"]
            self.assertTrue(gateway.report_delivery(job_id=result.job_id, nonce=nonce, ok=False))
        self.assertEqual(gateway.status()["backends"]["claude"]["health"], "open")

    def test_delivery_report_requires_real_nonce(self):
        gateway = self.gateway(forward_sink=None)
        self.assertFalse(gateway.report_delivery(job_id="tg-x", nonce="0" * 32, ok=False))
        self.assertEqual(gateway.status()["backends"]["claude"]["consecutive_failures"], 0)

    def test_timeout_counts_and_sets_timed_out(self):
        gateway = self.gateway(codex_runner=MockRunner(lambda spec: RunResult(False, "TIMEOUT")))
        result = gateway.handle(self.request(13, "#public frage"))
        self.assertEqual((result.status, result.code), ("timed_out", "TIMEOUT"))
        self.assertEqual(gateway.status()["backends"]["codex"]["consecutive_failures"], 1)

    def test_circuit_recovers(self):
        outcomes = iter([RunResult(False, "CRASHED")] * 3)
        from aria_router.runner import mock_codex_events
        runner = MockRunner(lambda spec: next(outcomes, RunResult(True, exit_code=0,
                                                                   stdout=mock_codex_events("WIEDER_DA"))))
        gateway = self.gateway(codex_runner=runner)
        for message_id in (14, 15, 16):
            gateway.handle(self.request(message_id, "#public frage"))
        self.assertEqual(gateway.handle(self.request(17, "#public frage")).backend, "claude")
        self.clock.advance(301)
        recovered = gateway.handle(self.request(18, "#public frage"))
        self.assertEqual((recovered.backend, recovered.reply_text), ("codex", "WIEDER_DA"))
        self.assertEqual(gateway.status()["backends"]["codex"]["health"], "closed")


if __name__ == "__main__":
    unittest.main()
