#!/usr/bin/env python3
"""
Tests for STORY-003: Circuit Breaker Integration
=================================================

Black Box Tests:
- Circuit breaker opens after failures
- Circuit breaker blocks requests when open
- Circuit breaker recovers to half-open

White Box Tests:
- State transitions CLOSED → OPEN → HALF_OPEN → CLOSED
- Failure recording
- Success recording
- Reset functionality
"""

import os
import sys
import time
import unittest
from unittest.mock import patch, MagicMock

# Add project root to path
sys.path.insert(0, "/mnt/e/genesis-system")

from core.qwen.unified_client import UnifiedQwenClient, get_qwen_client
from core.qwen.exceptions import QwenCircuitOpenError, QwenConnectionError
from core.retry_manager import CircuitState
import urllib.error


class TestCircuitBreakerBlackBox(unittest.TestCase):
    """Black box tests - test behavior without internal knowledge."""

    def setUp(self):
        """Reset singleton before each test."""
        UnifiedQwenClient.reset_singleton()

    def tearDown(self):
        """Reset singleton after each test."""
        UnifiedQwenClient.reset_singleton()

    def test_client_has_circuit_breaker(self):
        """Test client exposes circuit breaker methods."""
        client = UnifiedQwenClient()

        self.assertTrue(hasattr(client, "get_circuit_breaker_status"))
        self.assertTrue(hasattr(client, "reset_circuit_breaker"))

    def test_circuit_breaker_starts_closed(self):
        """Test circuit breaker starts in closed state."""
        client = UnifiedQwenClient()
        status = client.get_circuit_breaker_status()

        self.assertEqual(status["state"], "closed")
        self.assertEqual(status["failure_count"], 0)

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_circuit_breaker_opens_after_failures(self, mock_urlopen):
        """Test circuit opens after threshold failures."""
        mock_urlopen.side_effect = urllib.error.URLError("Connection refused")

        client = UnifiedQwenClient()
        # Default threshold is 3

        # Fail 3 times
        for i in range(3):
            try:
                client.generate_sync("test")
            except (QwenConnectionError, QwenCircuitOpenError):
                pass

        # Should be open now
        status = client.get_circuit_breaker_status()
        self.assertEqual(status["state"], "open")

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_circuit_breaker_blocks_when_open(self, mock_urlopen):
        """Test requests are blocked when circuit is open."""
        mock_urlopen.side_effect = urllib.error.URLError("Connection refused")

        client = UnifiedQwenClient()

        # Force open the circuit
        for _ in range(3):
            try:
                client.generate_sync("test")
            except (QwenConnectionError, QwenCircuitOpenError):
                pass

        # Next request should be blocked by circuit breaker
        with self.assertRaises(QwenCircuitOpenError) as ctx:
            client.generate_sync("test")

        self.assertIn("Circuit breaker is open", str(ctx.exception))

    def test_reset_circuit_breaker_works(self):
        """Test manual circuit breaker reset."""
        client = UnifiedQwenClient()

        # Manually set some failure state
        client._circuit_breaker._failure_count = 5
        client._circuit_breaker._state = CircuitState.OPEN

        # Reset
        client.reset_circuit_breaker()

        status = client.get_circuit_breaker_status()
        self.assertEqual(status["state"], "closed")
        self.assertEqual(status["failure_count"], 0)

    def test_status_includes_circuit_breaker(self):
        """Test get_status includes circuit breaker info."""
        client = UnifiedQwenClient()
        status = client.get_status()

        self.assertIn("circuit_breaker", status)
        self.assertIn("state", status["circuit_breaker"])


class TestCircuitBreakerWhiteBox(unittest.TestCase):
    """White box tests - test internal implementation."""

    def setUp(self):
        """Reset singleton before each test."""
        UnifiedQwenClient.reset_singleton()

    def tearDown(self):
        """Reset singleton after each test."""
        UnifiedQwenClient.reset_singleton()

    def test_circuit_breaker_state_closed_to_open(self):
        """Test transition from CLOSED to OPEN."""
        client = UnifiedQwenClient()
        cb = client._circuit_breaker

        self.assertEqual(cb.state, CircuitState.CLOSED)

        # Record failures up to threshold
        for _ in range(cb.failure_threshold):
            cb.record_failure()

        self.assertEqual(cb.state, CircuitState.OPEN)

    def test_circuit_breaker_state_open_to_half_open(self):
        """Test transition from OPEN to HALF_OPEN after timeout."""
        client = UnifiedQwenClient()
        cb = client._circuit_breaker

        # Force to open state
        cb._state = CircuitState.OPEN
        cb._last_failure_time = time.time() - (cb.recovery_timeout + 1)

        # Accessing state should trigger transition
        self.assertEqual(cb.state, CircuitState.HALF_OPEN)

    def test_circuit_breaker_state_half_open_to_closed(self):
        """Test transition from HALF_OPEN to CLOSED on success."""
        client = UnifiedQwenClient()
        cb = client._circuit_breaker

        # Force to half-open state
        cb._state = CircuitState.HALF_OPEN
        cb._half_open_calls = 0

        # Record enough successes
        for _ in range(cb.half_open_max_calls):
            cb.record_success()

        self.assertEqual(cb.state, CircuitState.CLOSED)

    def test_circuit_breaker_state_half_open_to_open(self):
        """Test transition from HALF_OPEN to OPEN on failure."""
        client = UnifiedQwenClient()
        cb = client._circuit_breaker

        # Force to half-open state
        cb._state = CircuitState.HALF_OPEN

        # Record failure
        cb.record_failure()

        self.assertEqual(cb.state, CircuitState.OPEN)

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_success_resets_failure_count(self, mock_urlopen):
        """Test successful request resets failure count."""
        mock_response = MagicMock()
        mock_response.read.return_value = b'{"response": "test"}'
        mock_response.__enter__ = MagicMock(return_value=mock_response)
        mock_response.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_response

        client = UnifiedQwenClient()

        # Set some failures (but below threshold)
        client._circuit_breaker._failure_count = 2

        # Successful request
        client.generate_sync("test")

        # Failure count should be reset
        self.assertEqual(client._circuit_breaker._failure_count, 0)

    def test_circuit_breaker_uses_config_values(self):
        """Test circuit breaker uses config values."""
        client = UnifiedQwenClient()
        cb = client._circuit_breaker
        config = client.config.get_circuit_breaker_config()

        self.assertEqual(cb.failure_threshold, config["failure_threshold"])
        self.assertEqual(cb.recovery_timeout, config["recovery_timeout"])
        self.assertEqual(cb.half_open_max_calls, config["half_open_max_calls"])


class TestCircuitBreakerErrorDetails(unittest.TestCase):
    """Test circuit breaker error details."""

    def setUp(self):
        UnifiedQwenClient.reset_singleton()

    def tearDown(self):
        UnifiedQwenClient.reset_singleton()

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_circuit_open_error_has_recovery_time(self, mock_urlopen):
        """Test QwenCircuitOpenError includes recovery time."""
        mock_urlopen.side_effect = urllib.error.URLError("fail")

        client = UnifiedQwenClient()

        # Force circuit open
        for _ in range(3):
            try:
                client.generate_sync("test")
            except (QwenConnectionError, QwenCircuitOpenError):
                pass

        # Catch the circuit open error
        try:
            client.generate_sync("test")
        except QwenCircuitOpenError as e:
            self.assertIsNotNone(e.recovery_time)
            self.assertIn("failure_count", e.details)
            self.assertIn("threshold", e.details)

    @patch("core.qwen.unified_client.urllib.request.urlopen")
    def test_circuit_open_error_shows_state(self, mock_urlopen):
        """Test error message includes circuit state."""
        mock_urlopen.side_effect = urllib.error.URLError("fail")

        client = UnifiedQwenClient()

        # Force circuit open
        for _ in range(3):
            try:
                client.generate_sync("test")
            except (QwenConnectionError, QwenCircuitOpenError):
                pass

        with self.assertRaises(QwenCircuitOpenError) as ctx:
            client.generate_sync("test")

        self.assertIn("open", str(ctx.exception).lower())


if __name__ == "__main__":
    unittest.main(verbosity=2)
