import unittest
from unittest.mock import patch

import pytest
import requests

from pypos.core.utils.http_retry import request_with_retry
from pypos.modules.sinkronisasi.services.sync_circuit_breaker_service import SyncCircuitBreakerService

# edited by glg
pytestmark = [pytest.mark.unit]


class _ResponseStub:
    def __init__(self, status_code=200):
        self.status_code = int(status_code)
        self.text = ""
        self.headers = {}

    def raise_for_status(self):
        if self.status_code >= 400:
            raise requests.exceptions.HTTPError(
                f"HTTP {self.status_code}",
                response=self,
            )


class _SessionStub:
    def __init__(self, statuses):
        self._statuses = list(statuses)
        self.calls = 0

    def request(self, **kwargs):
        _ = kwargs
        self.calls += 1
        if not self._statuses:
            return _ResponseStub(200)
        code = self._statuses.pop(0)
        if isinstance(code, Exception):
            raise code
        return _ResponseStub(code)


class HttpRetryHardeningTests(unittest.TestCase):
    def test_fail_fast_http_400_without_retry(self):
        session = _SessionStub([400, 200, 200])
        with self.assertRaises(requests.exceptions.HTTPError):
            request_with_retry(
                "POST",
                "https://example.test",
                session=session,
                max_retry=5,
                backoff_sec=0.01,
                retry_statuses=[408, 429, 500, 502, 503, 504],
            )
        self.assertEqual(session.calls, 1)

    def test_retry_http_503_until_success(self):
        session = _SessionStub([503, 503, 200])
        with patch("pypos.core.utils.http_retry.time.sleep") as sleep_mock:
            resp = request_with_retry(
                "POST",
                "https://example.test",
                session=session,
                max_retry=4,
                backoff_sec=1.0,
                backoff_factor=2.0,
                max_backoff_sec=30.0,
                jitter_ratio=0.0,
                retry_statuses=[503],
            )
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(session.calls, 3)
        self.assertEqual([call.args[0] for call in sleep_mock.call_args_list], [1.0, 2.0])


class SyncCircuitBreakerServiceTests(unittest.TestCase):
    def _build_service(self, state, threshold=2, open_seconds=120, statuses=None):
        statuses = list(statuses or [408, 429, 500, 502, 503, 504])

        def _get_state(key, default=None):
            return state.get(key, default)

        def _set_state(key, value):
            state[key] = value

        return SyncCircuitBreakerService(
            enabled_getter=lambda: True,
            threshold_getter=lambda: threshold,
            open_seconds_getter=lambda: open_seconds,
            retryable_status_getter=lambda: statuses,
            state_getter=_get_state,
            state_setter=_set_state,
            server_hash_getter=lambda: "serverhash",
        )

    def test_open_after_threshold_retryable_failure(self):
        state = {}
        service = self._build_service(state, threshold=2, open_seconds=120, statuses=[503])
        service.mark_failure(RuntimeError("status=503"))
        self.assertFalse(service.is_open())
        service.mark_failure(RuntimeError("status=503"))
        self.assertTrue(service.is_open())
        with self.assertRaises(RuntimeError):
            service.guard_or_raise()

    def test_non_retryable_failure_resets_counter(self):
        state = {}
        service = self._build_service(state, threshold=3, open_seconds=120, statuses=[503])
        service.mark_failure(RuntimeError("status=503"))
        self.assertEqual(service.get_failure_count(), 1)
        service.mark_failure(RuntimeError("status=400"))
        self.assertEqual(service.get_failure_count(), 0)
        self.assertFalse(service.is_open())

    def test_success_resets_open_state(self):
        state = {}
        service = self._build_service(state, threshold=2, open_seconds=120, statuses=[503])
        service.mark_failure(RuntimeError("status=503"))
        service.mark_failure(RuntimeError("status=503"))
        self.assertTrue(service.is_open())
        service.mark_success()
        self.assertFalse(service.is_open())
        self.assertEqual(service.get_failure_count(), 0)


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