"""AUTO-GENERATED by scripts/generate_sync.py - DO NOT EDIT.

Source: test_limiter.py

This module provides synchronous versions of the async classes.
Changes should be made to the source file, then regenerated.
"""

import time
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch

import pytest
from botocore.exceptions import ClientError

from zae_limiter import (
    CacheStats,
    Limit,
    LimiterInfo,
    OnUnavailable,
    RateLimiterUnavailable,
    RateLimitExceeded,
    SyncRateLimiter,
    ValidationError,
)
from zae_limiter.exceptions import InvalidIdentifierError, InvalidNameError
from zae_limiter.infra.sync_discovery import SyncInfrastructureDiscovery
from zae_limiter.models import BucketState
from zae_limiter.sync_repository_protocol import SpeculativeResult


class TestRateLimiterEntities:
    """Tests for entity management."""

    def test_create_entity(self, sync_limiter):
        """Test creating an entity."""
        entity = sync_limiter.create_entity(
            entity_id="proj-1", name="Test Project", metadata={"tier": "premium"}
        )
        assert entity.id == "proj-1"
        assert entity.name == "Test Project"
        assert entity.parent_id is None
        assert entity.metadata == {"tier": "premium"}

    def test_create_child_entity(self, sync_limiter):
        """Test creating a child entity."""
        sync_limiter.create_entity(entity_id="proj-1")
        child = sync_limiter.create_entity(entity_id="key-1", name="API Key 1", parent_id="proj-1")
        assert child.parent_id == "proj-1"

    def test_get_entity(self, sync_limiter):
        """Test getting an entity."""
        sync_limiter.create_entity(entity_id="proj-1", name="Test")
        entity = sync_limiter.get_entity("proj-1")
        assert entity is not None
        assert entity.id == "proj-1"

    def test_get_nonexistent_entity(self, sync_limiter):
        """Test getting a nonexistent entity."""
        entity = sync_limiter.get_entity("nonexistent")
        assert entity is None

    def test_get_children(self, sync_limiter):
        """Test getting children of a parent."""
        sync_limiter.create_entity(entity_id="proj-1")
        sync_limiter.create_entity(entity_id="key-1", parent_id="proj-1")
        sync_limiter.create_entity(entity_id="key-2", parent_id="proj-1")
        children = sync_limiter.get_children("proj-1")
        assert len(children) == 2
        child_ids = {c.id for c in children}
        assert child_ids == {"key-1", "key-2"}

    def test_delete_entity(self, sync_limiter):
        """Test deleting an entity."""
        sync_limiter.create_entity(entity_id="proj-1")
        sync_limiter.delete_entity("proj-1")
        entity = sync_limiter.get_entity("proj-1")
        assert entity is None


class TestRateLimiterAcquire:
    """Tests for acquire functionality."""

    def test_acquire_success(self, sync_limiter):
        """Test successful rate limit acquisition."""
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 1}
        ) as lease:
            assert lease.consumed == {"rpm": 1}

    def test_acquire_multiple_limits(self, sync_limiter):
        """Test acquiring multiple limits at once."""
        limits = [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 1, "tpm": 500}
        ) as lease:
            assert lease.consumed == {"rpm": 1, "tpm": 500}

    def test_acquire_exceeds_limit(self, sync_limiter):
        """Test that exceeding limit raises exception."""
        limits = [Limit.per_minute("rpm", 10)]
        with pytest.raises(RateLimitExceeded) as exc_info:
            with sync_limiter.acquire(
                entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 20}
            ):
                pass
        exc = exc_info.value
        assert len(exc.violations) == 1
        assert exc.violations[0].limit_name == "rpm"
        assert exc.violations[0].requested == 20
        assert exc.violations[0].available == 10
        assert exc.retry_after_seconds > 0

    def test_acquire_exception_includes_all_limits(self, sync_limiter):
        """Test that exception includes status of all limits."""
        limits = [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 100)]
        with pytest.raises(RateLimitExceeded) as exc_info:
            with sync_limiter.acquire(
                entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 1, "tpm": 200}
            ):
                pass
        exc = exc_info.value
        assert len(exc.statuses) == 2
        assert len(exc.violations) == 1
        assert len(exc.passed) == 1
        assert exc.passed[0].limit_name == "rpm"
        assert exc.violations[0].limit_name == "tpm"

    def test_acquire_rollback_on_exception(self, sync_limiter):
        """Test that consumption is rolled back on exception."""
        limits = [Limit.per_minute("rpm", 100)]
        try:
            with sync_limiter.acquire(
                entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 10}
            ):
                raise ValueError("Simulated error")
        except ValueError:
            pass
        available = sync_limiter.available(entity_id="key-1", resource="gpt-4", limits=limits)
        assert available["rpm"] == 100

    def test_acquire_fallback_when_batch_not_supported(self, sync_limiter, monkeypatch):
        """Test that acquire falls back to sequential get_buckets when batch not supported."""
        from zae_limiter.models import BackendCapabilities

        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 1}
        ) as lease:
            assert lease.consumed == {"rpm": 1}
        no_batch_capabilities = BackendCapabilities(
            supports_audit_logging=True,
            supports_usage_snapshots=True,
            supports_infrastructure_management=True,
            supports_change_streams=True,
            supports_batch_operations=False,
        )
        monkeypatch.setattr(sync_limiter._repository, "_capabilities", no_batch_capabilities)
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 1}
        ) as lease:
            assert lease.consumed == {"rpm": 1}


class TestRateLimiterLease:
    """Tests for SyncLease functionality."""

    def test_lease_consume(self, sync_limiter):
        """Test consuming additional tokens via lease."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"tpm": 500}
        ) as lease:
            lease.consume(tpm=500)
            assert lease.consumed == {"tpm": 1000}

    def test_lease_consume_exceeds_limit(self, sync_limiter):
        """Test that lease.consume raises when exceeding limit."""
        limits = [Limit.per_minute("tpm", 1000)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"tpm": 500}
        ) as lease:
            with pytest.raises(RateLimitExceeded):
                lease.consume(tpm=600)

    def test_lease_adjust(self, sync_limiter):
        """Test adjusting consumption (unchecked)."""
        limits = [Limit.per_minute("tpm", 1000)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"tpm": 500}
        ) as lease:
            lease.adjust(tpm=1000)
            assert lease.consumed == {"tpm": 1500}

    def test_lease_release(self, sync_limiter):
        """Test releasing tokens back."""
        limits = [Limit.per_minute("tpm", 1000)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"tpm": 500}
        ) as lease:
            lease.release(tpm=200)
            assert lease.consumed == {"tpm": 300}


class TestLeaseEdgeCases:
    """Tests for SyncLease edge cases (committed/rolled-back state, zero amounts)."""

    def test_consume_after_commit_raises(self, sync_limiter):
        """consume() on a committed lease raises RuntimeError."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-edge-1", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ) as lease:
            pass
        with pytest.raises(RuntimeError, match="no longer active"):
            lease.consume(tpm=50)

    def test_adjust_after_commit_raises(self, sync_limiter):
        """adjust() on a committed lease raises RuntimeError."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-edge-2", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ) as lease:
            pass
        with pytest.raises(RuntimeError, match="no longer active"):
            lease.adjust(tpm=50)

    def test_consume_zero_amount_is_noop(self, sync_limiter):
        """consume() with zero amount skips processing."""
        limits = [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-edge-3", resource="gpt-4", limits=limits, consume={"rpm": 1, "tpm": 100}
        ) as lease:
            lease.consume(rpm=1)
            assert lease.consumed == {"rpm": 2, "tpm": 100}

    def test_adjust_zero_amount_is_noop(self, sync_limiter):
        """adjust() with zero amount skips processing."""
        limits = [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-edge-4", resource="gpt-4", limits=limits, consume={"rpm": 1, "tpm": 100}
        ) as lease:
            lease.adjust(rpm=5)
            assert lease.consumed == {"rpm": 6, "tpm": 100}


class TestLeaseRetryPath:
    """Tests for lease _commit retry path and helpers (ADR-115)."""

    def test_is_condition_check_failure_by_class_name(self):
        """Detects ConditionalCheckFailedException by class name."""
        from zae_limiter.sync_lease import _is_condition_check_failure

        exc_cls = type("ConditionalCheckFailedException", (Exception,), {})
        assert _is_condition_check_failure(exc_cls()) is True

    def test_is_condition_check_failure_transaction_canceled_with_condition_reason(self):
        """TransactionCanceledException with ConditionalCheckFailed reason → True."""
        from zae_limiter.sync_lease import _is_condition_check_failure

        exc_cls = type("TransactionCanceledException", (Exception,), {})
        exc = exc_cls()
        exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}, {"Code": "None"}],
        }
        assert _is_condition_check_failure(exc) is True

    def test_is_condition_check_failure_transaction_conflict_only(self):
        """TransactionCanceledException with only TransactionConflict → False."""
        from zae_limiter.sync_lease import _is_condition_check_failure

        exc_cls = type("TransactionCanceledException", (Exception,), {})
        exc = exc_cls()
        exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "None"}, {"Code": "TransactionConflict"}],
        }
        assert _is_condition_check_failure(exc) is False

    def test_is_condition_check_failure_mixed_reasons(self):
        """Mixed ConditionalCheckFailed + TransactionConflict → True."""
        from zae_limiter.sync_lease import _is_condition_check_failure

        exc_cls = type("TransactionCanceledException", (Exception,), {})
        exc = exc_cls()
        exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [
                {"Code": "ConditionalCheckFailed"},
                {"Code": "TransactionConflict"},
            ],
        }
        assert _is_condition_check_failure(exc) is True

    def test_is_condition_check_failure_transaction_canceled_no_response(self):
        """TransactionCanceledException without response attribute → False."""
        from zae_limiter.sync_lease import _is_condition_check_failure

        exc_cls = type("TransactionCanceledException", (Exception,), {})
        assert _is_condition_check_failure(exc_cls()) is False

    def test_is_condition_check_failure_client_error(self):
        """Detects ConditionalCheckFailedException via botocore ClientError response."""
        from zae_limiter.sync_lease import _is_condition_check_failure

        exc = Exception("test")
        exc.response = {"Error": {"Code": "ConditionalCheckFailedException"}}
        assert _is_condition_check_failure(exc) is True

    def test_is_condition_check_failure_unrelated(self):
        """Returns False for unrelated exceptions."""
        from zae_limiter.sync_lease import _is_condition_check_failure

        assert _is_condition_check_failure(ValueError("test")) is False

    def test_is_transaction_conflict_pure_conflict(self):
        """TransactionCanceledException with TransactionConflict → True."""
        from zae_limiter.sync_lease import _is_transaction_conflict

        exc_cls = type("TransactionCanceledException", (Exception,), {})
        exc = exc_cls()
        exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "None"}, {"Code": "TransactionConflict"}],
        }
        assert _is_transaction_conflict(exc) is True

    def test_is_transaction_conflict_condition_check_only(self):
        """TransactionCanceledException with only ConditionalCheckFailed → False."""
        from zae_limiter.sync_lease import _is_transaction_conflict

        exc_cls = type("TransactionCanceledException", (Exception,), {})
        exc = exc_cls()
        exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}, {"Code": "None"}],
        }
        assert _is_transaction_conflict(exc) is False

    def test_is_transaction_conflict_unrelated(self):
        """Returns False for unrelated exceptions."""
        from zae_limiter.sync_lease import _is_transaction_conflict

        assert _is_transaction_conflict(ValueError("test")) is False

    def test_is_transaction_conflict_client_error(self):
        """TransactionConflict via botocore ClientError response."""
        from zae_limiter.sync_lease import _is_transaction_conflict

        exc = Exception("test")
        exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "TransactionConflict"}],
        }
        assert _is_transaction_conflict(exc) is True

    def test_build_retry_failure_statuses(self):
        """Builds LimitStatus list for retry failure with computed retry_after."""
        from zae_limiter.sync_lease import LeaseEntry, _build_retry_failure_statuses

        limit = Limit.per_minute("rpm", 100)
        state = MagicMock()
        state.tokens_milli = 50000
        entry = LeaseEntry(entity_id="e1", resource="gpt-4", limit=limit, state=state, consumed=60)
        statuses = _build_retry_failure_statuses([entry])
        assert len(statuses) == 1
        assert statuses[0].entity_id == "e1"
        assert statuses[0].available == 50
        assert statuses[0].requested == 60
        assert statuses[0].exceeded is True
        assert statuses[0].retry_after_seconds > 0.0

    def test_build_retry_failure_statuses_no_deficit(self):
        """retry_after_seconds is 0 when tokens are sufficient (shouldn't happen in practice)."""
        from zae_limiter.sync_lease import LeaseEntry, _build_retry_failure_statuses

        limit = Limit.per_minute("rpm", 100)
        state = MagicMock()
        state.tokens_milli = 100000
        entry = LeaseEntry(entity_id="e1", resource="gpt-4", limit=limit, state=state, consumed=10)
        statuses = _build_retry_failure_statuses([entry])
        assert statuses[0].retry_after_seconds == 0.0

    def test_commit_retry_on_condition_failure(self, sync_limiter):
        """Commit retries with consumption-only on optimistic lock failure."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ) as lease:
            assert lease.consumed == {"tpm": 100}

    def test_commit_retry_raises_rate_limit_exceeded(self):
        """When both normal and retry writes fail, raises RateLimitExceeded."""
        from zae_limiter.sync_lease import LeaseEntry, SyncLease

        limit = Limit.per_minute("rpm", 100)
        state = MagicMock()
        state.tokens_milli = 50000
        state.last_refill_ms = 1000
        state.total_consumed_milli = None
        entry = LeaseEntry(
            entity_id="e1",
            resource="gpt-4",
            limit=limit,
            state=state,
            consumed=10,
            _original_tokens_milli=100000,
            _original_rf_ms=1000,
        )
        mock_repo = MagicMock()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        condition_exc = exc_cls()
        condition_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}],
        }
        mock_repo.transact_write.side_effect = condition_exc
        mock_repo.build_composite_normal.return_value = {"Update": {}}
        mock_repo.build_composite_retry.return_value = {"Update": {}}
        mock_repo._bucket_ttl_refill_multiplier = 7
        lease = SyncLease(repository=mock_repo, entries=[entry])
        with pytest.raises(RateLimitExceeded):
            lease._commit_initial()


class TestWriteOnEnter:
    """Tests for write-on-enter behavior (Issue #309).

    Verifies _commit_initial(), _commit_adjustments(), and _rollback() paths.
    """

    def _make_entry(self, consumed=10, is_new=False, initial_consumed=0, entity_id="e1"):
        """Create a LeaseEntry with mock state."""
        from zae_limiter.sync_lease import LeaseEntry

        limit = Limit.per_minute("rpm", 100)
        state = MagicMock()
        state.tokens_milli = 90000
        state.last_refill_ms = 1000
        state.total_consumed_milli = None
        return LeaseEntry(
            entity_id=entity_id,
            resource="gpt-4",
            limit=limit,
            state=state,
            consumed=consumed,
            _original_tokens_milli=100000,
            _original_rf_ms=1000,
            _is_new=is_new,
            _initial_consumed=initial_consumed,
        )

    def _make_mock_repo(self):
        """Create a mock repository."""
        repo = MagicMock()
        repo.build_composite_normal.return_value = {"Update": {}}
        repo.build_composite_create.return_value = {"Put": {}}
        repo.build_composite_retry.return_value = {"Update": {}}
        repo.build_composite_adjust.return_value = {"Update": {}}
        repo._bucket_ttl_refill_multiplier = 7
        return repo

    def test_commit_initial_empty_entries(self):
        """_commit_initial is no-op with empty entries (lines 259-263)."""
        from zae_limiter.sync_lease import SyncLease

        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[])
        lease._commit_initial()
        assert lease._initial_committed is True
        mock_repo.transact_write.assert_not_called()

    def test_commit_initial_non_condition_check_reraises(self):
        """Non-condition-check exceptions propagate from _commit_initial (line 269)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry()
        mock_repo = self._make_mock_repo()
        mock_repo.transact_write.side_effect = RuntimeError("network error")
        lease = SyncLease(repository=mock_repo, entries=[entry])
        with pytest.raises(RuntimeError, match="network error"):
            lease._commit_initial()

    def test_commit_initial_create_race_retry(self):
        """Create race falls through to retry path (lines 275-278)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(is_new=True)
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        exc = exc_cls()
        exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}],
        }
        mock_repo.transact_write.side_effect = [exc, None]
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._commit_initial()
        assert lease._initial_committed is True
        assert mock_repo.transact_write.call_count == 2
        mock_repo.build_composite_retry.assert_called_once()

    def test_retry_non_condition_check_reraises(self):
        """Non-condition-check error in retry path propagates (line 301)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry()
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        condition_exc = exc_cls()
        condition_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}],
        }
        mock_repo.transact_write.side_effect = [condition_exc, RuntimeError("retry fail")]
        lease = SyncLease(repository=mock_repo, entries=[entry])
        with pytest.raises(RuntimeError, match="retry fail"):
            lease._commit_initial()

    def test_commit_adjustments_skips_when_committed(self):
        """_commit_adjustments is no-op when already committed (line 315)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=10, initial_consumed=5)
        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._committed = True
        lease._commit_adjustments()
        mock_repo.transact_write.assert_not_called()

    def test_commit_adjustments_skips_when_rolled_back(self):
        """_commit_adjustments is no-op when already rolled back (line 315)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=10, initial_consumed=5)
        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._rolled_back = True
        lease._commit_adjustments()
        mock_repo.transact_write.assert_not_called()

    def test_commit_adjustments_writes_delta(self):
        """_commit_adjustments writes delta when adjustments exist."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=15, initial_consumed=10)
        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._commit_adjustments()
        assert lease._committed is True
        mock_repo.build_composite_adjust.assert_called_once_with(
            entity_id="e1", resource="gpt-4", deltas={"rpm": 5000}
        )
        mock_repo.write_each.assert_called_once()

    def test_commit_adjustments_noop_when_no_change(self):
        """_commit_adjustments skips transact_write when no adjustments."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=10, initial_consumed=10)
        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._commit_adjustments()
        assert lease._committed is True
        mock_repo.transact_write.assert_not_called()

    def test_commit_adjustments_failure_allows_rollback(self):
        """_rollback works after _commit_adjustments fails (token leak fix)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=15, initial_consumed=10)
        mock_repo = self._make_mock_repo()
        mock_repo.write_each.side_effect = RuntimeError("network error")
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._initial_committed = True
        with pytest.raises(RuntimeError, match="network error"):
            lease._commit_adjustments()
        assert lease._committed is False
        mock_repo.write_each.side_effect = None
        lease._rollback()
        assert lease._rolled_back is True
        mock_repo.build_composite_adjust.assert_called_with(
            entity_id="e1", resource="gpt-4", deltas={"rpm": -10000}
        )

    def test_rollback_skips_when_committed(self):
        """_rollback is no-op when already committed (line 358)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=10, initial_consumed=10)
        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._committed = True
        lease._initial_committed = True
        lease._rollback()
        mock_repo.transact_write.assert_not_called()

    def test_rollback_skips_when_no_initial_commit(self):
        """_rollback is no-op when _initial_committed is False (line 364)."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=10, initial_consumed=10)
        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._rollback()
        assert lease._rolled_back is True
        mock_repo.transact_write.assert_not_called()

    def test_rollback_writes_compensating_delta(self):
        """_rollback writes negative delta to restore tokens."""
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=10, initial_consumed=10)
        mock_repo = self._make_mock_repo()
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._initial_committed = True
        lease._rollback()
        assert lease._rolled_back is True
        mock_repo.build_composite_adjust.assert_called_once_with(
            entity_id="e1", resource="gpt-4", deltas={"rpm": -10000}
        )
        mock_repo.write_each.assert_called_once()

    def test_rollback_failure_logs_warning(self, caplog):
        """_rollback logs warning and doesn't raise on write_each failure (lines 394-395)."""
        import logging

        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry(consumed=10, initial_consumed=10)
        mock_repo = self._make_mock_repo()
        mock_repo.write_each.side_effect = RuntimeError("DynamoDB down")
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._initial_committed = True
        with caplog.at_level(logging.WARNING, logger="zae_limiter.sync_lease"):
            lease._rollback()
        assert lease._rolled_back is True
        assert "Failed to rollback consumed tokens" in caplog.text

    def test_cascade_normal_fails_retry_succeeds(self):
        """Cascade: normal path fails (optimistic lock), retry path succeeds.

        Two entries with different entity_ids (child + parent) simulate a
        cascade scenario where the shared parent bucket causes the normal
        transact_write to fail with ConditionalCheckFailed. The retry
        path uses build_composite_retry for both entries and succeeds.
        """
        from zae_limiter.sync_lease import SyncLease

        child_entry = self._make_entry(consumed=5, entity_id="child-1")
        parent_entry = self._make_entry(consumed=5, entity_id="parent-1")
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        condition_exc = exc_cls()
        condition_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}, {"Code": "None"}],
        }
        mock_repo.transact_write.side_effect = [condition_exc, None]
        lease = SyncLease(repository=mock_repo, entries=[child_entry, parent_entry])
        lease._commit_initial()
        assert lease._initial_committed is True
        assert mock_repo.transact_write.call_count == 2
        assert mock_repo.build_composite_retry.call_count == 2

    def test_cascade_both_paths_fail_raises_rate_limit_exceeded(self):
        """Cascade: both normal and retry paths fail → RateLimitExceeded.

        When both transact_write calls fail with condition check failures,
        _commit_initial raises RateLimitExceeded with statuses for all entries.
        """
        from zae_limiter.sync_lease import SyncLease

        child_entry = self._make_entry(consumed=5, entity_id="child-1")
        parent_entry = self._make_entry(consumed=5, entity_id="parent-1")
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        condition_exc = exc_cls()
        condition_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}],
        }
        mock_repo.transact_write.side_effect = condition_exc
        lease = SyncLease(repository=mock_repo, entries=[child_entry, parent_entry])
        with pytest.raises(RateLimitExceeded) as exc_info:
            lease._commit_initial()
        assert len(exc_info.value.statuses) == 2
        entity_ids = {s.entity_id for s in exc_info.value.statuses}
        assert entity_ids == {"child-1", "parent-1"}

    def test_commit_initial_transaction_conflict_retries_original(self):
        """TransactionConflict retries original transaction, not consumption-only.

        When transact_write fails with TransactionConflict (not ConditionalCheckFailed),
        _commit_initial should retry the same transaction with backoff rather than
        falling through to the retry (consumption-only) path.
        """
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry()
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        conflict_exc = exc_cls()
        conflict_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "None"}, {"Code": "TransactionConflict"}],
        }
        mock_repo.transact_write.side_effect = [conflict_exc, None]
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._commit_initial()
        assert lease._initial_committed is True
        assert mock_repo.transact_write.call_count == 2
        mock_repo.build_composite_retry.assert_not_called()

    def test_commit_initial_transaction_conflict_exhausts_retries(self):
        """TransactionConflict exhausts retries then propagates exception.

        After max retries on TransactionConflict, the exception should propagate
        rather than entering the consumption-only retry path.
        """
        from zae_limiter.sync_lease import _CONFLICT_MAX_RETRIES, SyncLease

        entry = self._make_entry()
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        conflict_exc = exc_cls()
        conflict_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "TransactionConflict"}],
        }
        mock_repo.transact_write.side_effect = conflict_exc
        lease = SyncLease(repository=mock_repo, entries=[entry])
        with pytest.raises(type(conflict_exc)):
            lease._commit_initial()
        assert mock_repo.transact_write.call_count == _CONFLICT_MAX_RETRIES + 1
        mock_repo.build_composite_retry.assert_not_called()

    def test_commit_initial_condition_check_still_enters_retry_path(self):
        """ConditionalCheckFailed still enters consumption-only retry path.

        Regression test: the TransactionConflict fix should not break the
        existing ConditionalCheckFailed → retry path behavior.
        """
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry()
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        condition_exc = exc_cls()
        condition_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "ConditionalCheckFailed"}, {"Code": "None"}],
        }
        mock_repo.transact_write.side_effect = [condition_exc, None]
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._commit_initial()
        assert lease._initial_committed is True
        assert mock_repo.transact_write.call_count == 2
        mock_repo.build_composite_retry.assert_called_once()

    def test_commit_initial_mixed_reasons_prefers_condition_check(self):
        """Mixed ConditionalCheckFailed + TransactionConflict enters retry path.

        When both codes are present in CancellationReasons, ConditionalCheckFailed
        takes precedence — the consumption-only retry path should be used rather
        than the TransactionConflict retry loop.
        """
        from zae_limiter.sync_lease import SyncLease

        entry = self._make_entry()
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        mixed_exc = exc_cls()
        mixed_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [
                {"Code": "ConditionalCheckFailed"},
                {"Code": "TransactionConflict"},
            ],
        }
        mock_repo.transact_write.side_effect = [mixed_exc, None]
        lease = SyncLease(repository=mock_repo, entries=[entry])
        lease._commit_initial()
        assert lease._initial_committed is True
        assert mock_repo.transact_write.call_count == 2
        mock_repo.build_composite_retry.assert_called_once()

    def test_cascade_transaction_conflict_does_not_raise_rate_limit_exceeded(self):
        """Cascade TransactionConflict does not raise false RateLimitExceeded (Issue #332).

        When cascade transact_write fails with TransactionConflict (parent bucket
        contention), it should retry the original transaction, not enter the
        consumption-only path that would raise false RateLimitExceeded.
        """
        from zae_limiter.sync_lease import SyncLease

        child_entry = self._make_entry(consumed=5, entity_id="child-1")
        parent_entry = self._make_entry(consumed=5, entity_id="parent-1")
        mock_repo = self._make_mock_repo()
        exc_cls = type("TransactionCanceledException", (Exception,), {})
        conflict_exc = exc_cls()
        conflict_exc.response = {
            "Error": {"Code": "TransactionCanceledException"},
            "CancellationReasons": [{"Code": "None"}, {"Code": "TransactionConflict"}],
        }
        mock_repo.transact_write.side_effect = [conflict_exc, None]
        lease = SyncLease(repository=mock_repo, entries=[child_entry, parent_entry])
        lease._commit_initial()
        assert lease._initial_committed is True
        assert mock_repo.transact_write.call_count == 2
        mock_repo.build_composite_retry.assert_not_called()

    def test_acquire_writes_on_enter(self, sync_limiter):
        """Tokens are consumed in DynamoDB immediately on context enter."""
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="enter-test", resource="gpt-4", limits=limits, consume={"rpm": 10}
        ):
            buckets = sync_limiter._repository.get_buckets(entity_id="enter-test", resource="gpt-4")
            bucket = next(b for b in buckets if b.limit_name == "rpm")
            assert bucket.tokens_milli <= 90000

    def test_acquire_rollback_restores_on_error(self, sync_limiter):
        """Rollback writes compensating transaction on error."""
        limits = [Limit.per_minute("rpm", 100)]
        try:
            with sync_limiter.acquire(
                entity_id="rollback-test", resource="gpt-4", limits=limits, consume={"rpm": 10}
            ):
                raise ValueError("boom")
        except ValueError:
            pass
        available = sync_limiter.available(
            entity_id="rollback-test", resource="gpt-4", limits=limits
        )
        assert available["rpm"] == 100

    def test_cascade_writes_both_on_enter(self, sync_limiter):
        """Cascade writes both child and parent buckets on enter."""
        sync_limiter.create_entity(entity_id="proj-cascade")
        sync_limiter.create_entity(entity_id="key-cascade", parent_id="proj-cascade", cascade=True)
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="key-cascade", resource="gpt-4", limits=limits, consume={"rpm": 5}
        ):
            child_available = sync_limiter.available(
                entity_id="key-cascade", resource="gpt-4", limits=limits
            )
            parent_available = sync_limiter.available(
                entity_id="proj-cascade", resource="gpt-4", limits=limits
            )
            assert child_available["rpm"] == 95
            assert parent_available["rpm"] == 95


class TestRateLimiterLeaseCounter:
    """Tests for consumption counter tracking (issue #179).

    The counter tracks net consumption in millitokens, stored as a flat
    top-level DynamoDB attribute to enable atomic ADD operations.
    """

    def test_acquire_initializes_counter(self, sync_limiter):
        """Initial acquire initializes counter to consumed amount."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="counter-test-1", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ):
            pass
        buckets = sync_limiter._repository.get_buckets(entity_id="counter-test-1", resource="gpt-4")
        bucket = next(b for b in buckets if b.limit_name == "tpm")
        assert bucket.total_consumed_milli == 100000

    def test_lease_consume_increments_counter(self, sync_limiter):
        """Additional consume() calls increment the counter."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="counter-test-2", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ) as lease:
            lease.consume(tpm=50)
        buckets = sync_limiter._repository.get_buckets(entity_id="counter-test-2", resource="gpt-4")
        bucket = next(b for b in buckets if b.limit_name == "tpm")
        assert bucket.total_consumed_milli == 150000

    def test_lease_adjust_negative_decrements_counter(self, sync_limiter):
        """Negative adjust() decrements counter (net tracking)."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="counter-test-3", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ) as lease:
            lease.adjust(tpm=-30)
        buckets = sync_limiter._repository.get_buckets(entity_id="counter-test-3", resource="gpt-4")
        bucket = next(b for b in buckets if b.limit_name == "tpm")
        assert bucket.total_consumed_milli == 70000

    def test_lease_release_decrements_counter(self, sync_limiter):
        """release() decrements counter (same as negative adjust)."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="counter-test-4", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ) as lease:
            lease.release(tpm=40)
        buckets = sync_limiter._repository.get_buckets(entity_id="counter-test-4", resource="gpt-4")
        bucket = next(b for b in buckets if b.limit_name == "tpm")
        assert bucket.total_consumed_milli == 60000

    def test_lease_adjust_positive_increments_counter(self, sync_limiter):
        """Positive adjust() increments counter (same as consume)."""
        limits = [Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="counter-test-5", resource="gpt-4", limits=limits, consume={"tpm": 100}
        ) as lease:
            lease.adjust(tpm=200)
        buckets = sync_limiter._repository.get_buckets(entity_id="counter-test-5", resource="gpt-4")
        bucket = next(b for b in buckets if b.limit_name == "tpm")
        assert bucket.total_consumed_milli == 300000


class TestRateLimiterCascade:
    """Tests for cascade functionality (entity-level cascade)."""

    def test_cascade_consumes_parent(self, sync_limiter):
        """Test that entity with cascade=True consumes from parent too."""
        sync_limiter.create_entity(entity_id="proj-1")
        sync_limiter.create_entity(entity_id="key-1", parent_id="proj-1", cascade=True)
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 1}
        ):
            pass
        child_available = sync_limiter.available(entity_id="key-1", resource="gpt-4", limits=limits)
        parent_available = sync_limiter.available(
            entity_id="proj-1", resource="gpt-4", limits=limits
        )
        assert child_available["rpm"] == 99
        assert parent_available["rpm"] == 99

    def test_no_cascade_by_default(self, sync_limiter):
        """Test that entities without cascade=True do NOT cascade."""
        sync_limiter.create_entity(entity_id="proj-1")
        sync_limiter.create_entity(entity_id="key-1", parent_id="proj-1")
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 1}
        ):
            pass
        child_available = sync_limiter.available(entity_id="key-1", resource="gpt-4", limits=limits)
        parent_available = sync_limiter.available(
            entity_id="proj-1", resource="gpt-4", limits=limits
        )
        assert child_available["rpm"] == 99
        assert parent_available["rpm"] == 100

    def test_cascade_parent_limit_exceeded(self, sync_limiter):
        """Test that parent limit can block child when cascade is enabled."""
        sync_limiter.create_entity(entity_id="proj-1")
        sync_limiter.create_entity(entity_id="key-1", parent_id="proj-1", cascade=True)
        child_limits = [Limit.per_minute("rpm", 100)]
        parent_limits = [Limit.per_minute("rpm", 5)]
        sync_limiter.set_limits("proj-1", parent_limits)
        with sync_limiter.acquire(
            entity_id="proj-1", resource="gpt-4", limits=parent_limits, consume={"rpm": 5}
        ):
            pass
        with pytest.raises(RateLimitExceeded) as exc_info:
            with sync_limiter.acquire(
                entity_id="key-1", resource="gpt-4", limits=child_limits, consume={"rpm": 1}
            ):
                pass
        exc = exc_info.value
        assert any(v.entity_id == "proj-1" for v in exc.violations)

    def test_cascade_entity_without_parent(self, sync_limiter):
        """Test that cascade=True on entity without parent is harmless."""
        sync_limiter.create_entity(entity_id="orphan-1", cascade=True)
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="orphan-1", resource="gpt-4", limits=limits, consume={"rpm": 1}
        ):
            pass
        available = sync_limiter.available(entity_id="orphan-1", resource="gpt-4", limits=limits)
        assert available["rpm"] == 99

    def test_backward_compat_missing_cascade_field(self, sync_limiter):
        """Test that entities without cascade field default to False."""
        entity = sync_limiter.create_entity(entity_id="legacy-1", parent_id=None)
        assert entity.cascade is False


class TestRateLimiterStoredLimits:
    """Tests for stored limit configs."""

    def test_set_and_get_limits(self, sync_limiter):
        """Test storing and retrieving limits."""
        limits = [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 10000)]
        sync_limiter.set_limits("key-1", limits, resource="gpt-4")
        retrieved = sync_limiter.get_limits("key-1", resource="gpt-4")
        assert len(retrieved) == 2
        names = {limit.name for limit in retrieved}
        assert names == {"rpm", "tpm"}

    def test_use_stored_limits(self, sync_limiter):
        """Test using stored limits in acquire."""
        stored_limits = [Limit.per_minute("rpm", 500)]
        sync_limiter.set_limits("key-1", stored_limits, resource="gpt-4")
        with pytest.deprecated_call():
            with sync_limiter.acquire(
                entity_id="key-1", resource="gpt-4", consume={"rpm": 200}, use_stored_limits=True
            ):
                pass

    def test_delete_limits(self, sync_limiter):
        """Test deleting stored limits."""
        limits = [Limit.per_minute("rpm", 100)]
        sync_limiter.set_limits("key-1", limits)
        sync_limiter.delete_limits("key-1")
        retrieved = sync_limiter.get_limits("key-1")
        assert len(retrieved) == 0

    def test_use_stored_limits_available_deprecation(self, sync_limiter):
        """Test that use_stored_limits in available() emits deprecation warning."""
        import warnings

        limits = [Limit.per_minute("rpm", 100)]
        sync_limiter.set_limits("key-1", limits, resource="gpt-4")
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            sync_limiter.available(
                entity_id="key-1", resource="gpt-4", limits=limits, use_stored_limits=True
            )
            assert len(w) == 1
            assert issubclass(w[0].category, DeprecationWarning)
            assert "use_stored_limits is deprecated" in str(w[0].message)

    def test_use_stored_limits_time_until_available_deprecation(self, sync_limiter):
        """Test that use_stored_limits in time_until_available() emits deprecation warning."""
        import warnings

        limits = [Limit.per_minute("rpm", 100)]
        sync_limiter.set_limits("key-1", limits, resource="gpt-4")
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            sync_limiter.time_until_available(
                entity_id="key-1",
                resource="gpt-4",
                limits=limits,
                needed={"rpm": 1},
                use_stored_limits=True,
            )
            assert len(w) == 1
            assert issubclass(w[0].category, DeprecationWarning)
            assert "use_stored_limits is deprecated" in str(w[0].message)


class TestRateLimiterResourceDefaults:
    """Tests for resource-level default configs."""

    def test_set_and_get_resource_defaults(self, sync_limiter):
        """Test storing and retrieving resource-level defaults."""
        limits = [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 10000)]
        sync_limiter.set_resource_defaults("gpt-4", limits)
        retrieved = sync_limiter.get_resource_defaults("gpt-4")
        assert len(retrieved) == 2
        names = {limit.name for limit in retrieved}
        assert names == {"rpm", "tpm"}

    def test_delete_resource_defaults(self, sync_limiter):
        """Test deleting resource-level defaults."""
        limits = [Limit.per_minute("rpm", 100)]
        sync_limiter.set_resource_defaults("gpt-4", limits)
        sync_limiter.delete_resource_defaults("gpt-4")
        retrieved = sync_limiter.get_resource_defaults("gpt-4")
        assert len(retrieved) == 0

    def test_get_resource_defaults_empty(self, sync_limiter):
        """Test getting resource defaults when none exist."""
        retrieved = sync_limiter.get_resource_defaults("nonexistent")
        assert len(retrieved) == 0

    def test_list_resources_with_defaults(self, sync_limiter):
        """Test listing resources with configured defaults."""
        resources = sync_limiter.list_resources_with_defaults()
        assert len(resources) == 0
        limits = [Limit.per_minute("rpm", 100)]
        sync_limiter.set_resource_defaults("gpt-4", limits)
        sync_limiter.set_resource_defaults("claude-3", limits)
        resources = sync_limiter.list_resources_with_defaults()
        assert "gpt-4" in resources
        assert "claude-3" in resources

    def test_resource_defaults_replace_on_update(self, sync_limiter):
        """Test that setting defaults replaces existing ones."""
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 100)])
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("tpm", 5000)])
        retrieved = sync_limiter.get_resource_defaults("gpt-4")
        assert len(retrieved) == 1
        assert retrieved[0].name == "tpm"


class TestRateLimiterSystemDefaults:
    """Tests for system-level default configs."""

    def test_set_and_get_system_defaults(self, sync_limiter):
        """Test storing and retrieving system-level defaults."""
        limits = [Limit.per_minute("rpm", 50), Limit.per_minute("tpm", 5000)]
        sync_limiter.set_system_defaults(limits)
        retrieved, on_unavailable = sync_limiter.get_system_defaults()
        assert len(retrieved) == 2
        names = {limit.name for limit in retrieved}
        assert names == {"rpm", "tpm"}
        assert on_unavailable is None

    def test_set_system_defaults_with_on_unavailable(self, sync_limiter):
        """Test storing system defaults with on_unavailable config."""
        from zae_limiter import OnUnavailable

        limits = [Limit.per_minute("rpm", 50)]
        sync_limiter.set_system_defaults(limits, on_unavailable=OnUnavailable.ALLOW)
        retrieved, on_unavailable = sync_limiter.get_system_defaults()
        assert len(retrieved) == 1
        assert on_unavailable == OnUnavailable.ALLOW

    def test_delete_system_defaults(self, sync_limiter):
        """Test deleting system-level defaults."""
        limits = [Limit.per_minute("rpm", 50)]
        sync_limiter.set_system_defaults(limits)
        sync_limiter.delete_system_defaults()
        retrieved, on_unavailable = sync_limiter.get_system_defaults()
        assert len(retrieved) == 0
        assert on_unavailable is None

    def test_get_system_defaults_empty(self, sync_limiter):
        """Test getting system defaults when none exist."""
        retrieved, on_unavailable = sync_limiter.get_system_defaults()
        assert len(retrieved) == 0
        assert on_unavailable is None

    def test_system_defaults_replace_on_update(self, sync_limiter):
        """Test that setting defaults replaces existing ones."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 50)])
        sync_limiter.set_system_defaults([Limit.per_minute("tpm", 2500)])
        retrieved, _ = sync_limiter.get_system_defaults()
        assert len(retrieved) == 1
        assert retrieved[0].name == "tpm"


class TestRateLimiterFourTierResolution:
    """Tests for four-tier limit resolution: Entity > Entity Default > Resource > System."""

    def test_resolution_entity_level(self, sync_limiter):
        """Test that entity-level limits take precedence."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 50)])
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="gpt-4")
        with sync_limiter.acquire(
            entity_id="user-1", resource="gpt-4", limits=None, consume={"rpm": 75}
        ):
            pass

    def test_resolution_entity_default_fallback(self, sync_limiter):
        """Entity _default_ config is used when no resource-specific entity config exists.

        Resolution for acquire(entity_id="user-1", resource="gpt-4"):
        1. Entity config for "gpt-4"? -> No
        2. Entity config for "_default_"? -> Yes (100 rpm) <- USED
        3. Resource defaults for "gpt-4"? -> Yes (50 rpm)
        4. System defaults? -> Yes (10 rpm)

        Entity's _default_ config (100 rpm) takes precedence over resource defaults (50 rpm).
        """
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 50)])
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="_default_")
        with sync_limiter.acquire(
            entity_id="user-1", resource="gpt-4", limits=None, consume={"rpm": 75}
        ):
            pass

    def test_resolution_resource_level_fallback(self, sync_limiter):
        """Test that resource-level limits are used when no entity limits exist."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 50)])
        with sync_limiter.acquire(
            entity_id="user-2", resource="gpt-4", limits=None, consume={"rpm": 25}
        ):
            pass

    def test_resolution_system_level_fallback(self, sync_limiter):
        """Test that system-level limits are used when no entity/resource limits exist."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire(
            entity_id="user-3", resource="claude-3", limits=None, consume={"rpm": 50}
        ):
            pass

    def test_resolution_override_fallback(self, sync_limiter):
        """Test that override parameter is used when no stored config exists."""
        with sync_limiter.acquire(
            entity_id="user-4",
            resource="new-resource",
            limits=[Limit.per_minute("rpm", 100)],
            consume={"rpm": 50},
        ):
            pass

    def test_resolution_no_limits_raises_validation_error(self, sync_limiter):
        """Test that ValidationError is raised when no limits found anywhere."""
        with pytest.raises(ValidationError) as exc_info:
            with sync_limiter.acquire(
                entity_id="user-5", resource="unknown-resource", limits=None, consume={"rpm": 1}
            ):
                pass
        assert "No limits configured" in str(exc_info.value)
        assert "user-5" in str(exc_info.value)
        assert "unknown-resource" in str(exc_info.value)

    def test_on_unavailable_resolution_from_system_config(self, sync_limiter):
        """Test that on_unavailable is resolved from system config."""
        from zae_limiter import OnUnavailable

        sync_limiter.set_system_defaults(
            [Limit.per_minute("rpm", 100)], on_unavailable=OnUnavailable.ALLOW
        )
        with sync_limiter.acquire(
            entity_id="user-6", resource="gpt-4", limits=None, consume={"rpm": 1}
        ):
            pass

    def test_available_uses_resolution(self, sync_limiter):
        """Test that available() also uses three-tier resolution."""
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 100)])
        available = sync_limiter.available(entity_id="user-7", resource="gpt-4", limits=None)
        assert available["rpm"] == 100

    def test_time_until_available_uses_resolution(self, sync_limiter):
        """Test that time_until_available() also uses three-tier resolution."""
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 100)])
        wait_time = sync_limiter.time_until_available(
            entity_id="user-8", resource="gpt-4", needed={"rpm": 50}, limits=None
        )
        assert wait_time == 0.0


class TestRateLimiterCapacity:
    """Tests for capacity queries."""

    def test_available(self, sync_limiter):
        """Test checking available capacity."""
        limits = [Limit.per_minute("rpm", 100)]
        available = sync_limiter.available(entity_id="key-1", resource="gpt-4", limits=limits)
        assert available["rpm"] == 100
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 30}
        ):
            pass
        available = sync_limiter.available(entity_id="key-1", resource="gpt-4", limits=limits)
        assert available["rpm"] == 70

    def test_time_until_available(self, sync_limiter):
        """Test calculating time until capacity available."""
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 100}
        ):
            pass
        wait = sync_limiter.time_until_available(
            entity_id="key-1", resource="gpt-4", limits=limits, needed={"rpm": 50}
        )
        assert 29 < wait < 31


class TestRateLimitExceededException:
    """Tests for RateLimitExceeded exception."""

    def test_as_dict(self, sync_limiter):
        """Test exception serialization."""
        limits = [Limit.per_minute("rpm", 10)]
        try:
            with sync_limiter.acquire(
                entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 20}
            ):
                pass
        except RateLimitExceeded as e:
            data = e.as_dict()
            assert data["error"] == "rate_limit_exceeded"
            assert "retry_after_seconds" in data
            assert "retry_after_ms" in data
            assert len(data["limits"]) == 1
            assert data["limits"][0]["exceeded"] is True

    def test_retry_after_header(self, sync_limiter):
        """Test retry_after_header property."""
        limits = [Limit.per_minute("rpm", 10)]
        try:
            with sync_limiter.acquire(
                entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 20}
            ):
                pass
        except RateLimitExceeded as e:
            header = e.retry_after_header
            assert header.isdigit()
            assert int(header) > 0


class TestRateLimiterOnUnavailable:
    """Tests for ALLOW vs BLOCK behavior when DynamoDB is unavailable."""

    def test_allow_returns_noop_lease_on_dynamodb_error(self, sync_limiter, monkeypatch):
        """ALLOW should return no-op lease on infrastructure error."""

        def mock_error(*args, **kwargs):
            raise ClientError(
                {"Error": {"Code": "ServiceUnavailable", "Message": "DynamoDB down"}},
                "BatchGetItem",
            )

        monkeypatch.setattr(sync_limiter._repository, "batch_get_entity_and_buckets", mock_error)
        monkeypatch.setattr(
            sync_limiter._repository, "resolve_on_unavailable", MagicMock(return_value="allow")
        )
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="test-entity", resource="api", limits=limits, consume={"rpm": 1}
        ) as lease:
            assert len(lease.entries) == 0
            assert lease.consumed == {}

    def test_block_raises_unavailable_on_dynamodb_error(self, sync_limiter, monkeypatch):
        """BLOCK should reject requests when DynamoDB is down."""

        def mock_error(*args, **kwargs):
            raise ClientError(
                {"Error": {"Code": "ProvisionedThroughputExceededException"}}, "BatchGetItem"
            )

        monkeypatch.setattr(sync_limiter._repository, "batch_get_entity_and_buckets", mock_error)
        limits = [Limit.per_minute("rpm", 100)]
        with pytest.raises(RateLimiterUnavailable) as exc_info:
            with sync_limiter.acquire(
                entity_id="test-entity", resource="api", limits=limits, consume={"rpm": 1}
            ):
                pass
        assert exc_info.value.cause is not None
        assert "ProvisionedThroughputExceededException" in str(exc_info.value.cause)

    def test_allow_override_in_acquire_call(self, sync_limiter, monkeypatch):
        """on_unavailable parameter should override limiter default."""

        def mock_error(*args, **kwargs):
            raise ClientError({"Error": {"Code": "InternalServerError"}}, "BatchGetItem")

        monkeypatch.setattr(sync_limiter._repository, "batch_get_entity_and_buckets", mock_error)
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire(
            entity_id="test-entity",
            resource="api",
            limits=limits,
            consume={"rpm": 1},
            on_unavailable=OnUnavailable.ALLOW,
        ) as lease:
            assert len(lease.entries) == 0

    def test_block_override_in_acquire_call(self, sync_limiter, monkeypatch):
        """on_unavailable parameter should override limiter default."""

        def mock_error(*args, **kwargs):
            raise Exception("DynamoDB timeout")

        monkeypatch.setattr(sync_limiter._repository, "batch_get_entity_and_buckets", mock_error)
        monkeypatch.setattr(
            sync_limiter._repository, "resolve_on_unavailable", MagicMock(return_value="allow")
        )
        limits = [Limit.per_minute("rpm", 100)]
        with pytest.raises(RateLimiterUnavailable):
            with sync_limiter.acquire(
                entity_id="test-entity",
                resource="api",
                limits=limits,
                consume={"rpm": 1},
                on_unavailable=OnUnavailable.BLOCK,
            ):
                pass


class TestRateLimiterStackOptions:
    """Tests for stack_options initialization."""

    def test_limiter_with_stack_options_calls_ensure_infrastructure(
        self, mock_dynamodb, monkeypatch
    ):
        """When stack_options is provided, _ensure_initialized calls ensure_infrastructure."""
        from unittest.mock import MagicMock

        from zae_limiter import StackOptions, SyncRateLimiter

        stack_options = StackOptions(lambda_timeout=120)
        limiter = SyncRateLimiter(
            name="test-with-stack-options", region="us-east-1", stack_options=stack_options
        )
        ensure_infrastructure_mock = MagicMock(return_value=None)
        monkeypatch.setattr(
            limiter._repository, "ensure_infrastructure", ensure_infrastructure_mock
        )
        limiter._ensure_initialized()
        ensure_infrastructure_mock.assert_called_once()
        limiter.close()

    def test_limiter_without_stack_options_calls_ensure_infrastructure(
        self, mock_dynamodb, monkeypatch
    ):
        """When stack_options is None, _ensure_initialized still calls ensure_infrastructure.

        The ensure_infrastructure method is always called by SyncRateLimiter, but it's
        a no-op when SyncRepository was created without stack_options.
        """
        from unittest.mock import MagicMock

        from zae_limiter import SyncRateLimiter

        limiter = SyncRateLimiter(
            name="test-without-stack-options", region="us-east-1", stack_options=None
        )
        ensure_infrastructure_mock = MagicMock(return_value=None)
        monkeypatch.setattr(
            limiter._repository, "ensure_infrastructure", ensure_infrastructure_mock
        )
        limiter._ensure_initialized()
        ensure_infrastructure_mock.assert_called_once()
        limiter.close()


class TestRateLimiterResourceCapacity:
    """Tests for get_resource_capacity."""

    def test_get_resource_capacity_basic_aggregation(self, sync_limiter):
        """Should aggregate capacity across all entities for a resource."""
        entities = ["entity-a", "entity-b", "entity-c"]
        for entity_id in entities:
            sync_limiter.create_entity(entity_id)
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire("entity-a", "gpt-4", {"rpm": 20}, limits=limits):
            pass
        with sync_limiter.acquire("entity-b", "gpt-4", {"rpm": 50}, limits=limits):
            pass
        with sync_limiter.acquire("entity-c", "gpt-4", {"rpm": 10}, limits=limits):
            pass
        capacity = sync_limiter.get_resource_capacity(resource="gpt-4", limit_name="rpm")
        assert capacity.resource == "gpt-4"
        assert capacity.limit_name == "rpm"
        assert capacity.total_capacity == 300
        assert capacity.total_available == 220
        assert len(capacity.entities) == 3
        entity_map = {e.entity_id: e for e in capacity.entities}
        assert entity_map["entity-a"].available == 80
        assert entity_map["entity-b"].available == 50
        assert entity_map["entity-c"].available == 90

    def test_get_resource_capacity_parents_only_filter(self, sync_limiter):
        """parents_only=True should exclude child entities."""
        sync_limiter.create_entity("org-1")
        sync_limiter.create_entity("team-1", parent_id="org-1")
        sync_limiter.create_entity("org-2")
        limits = [Limit.per_minute("rpm", 100)]
        for entity_id in ["org-1", "team-1", "org-2"]:
            with sync_limiter.acquire(entity_id, "api", {"rpm": 10}, limits=limits):
                pass
        all_capacity = sync_limiter.get_resource_capacity("api", "rpm", parents_only=False)
        assert len(all_capacity.entities) == 3
        assert all_capacity.total_capacity == 300
        parent_capacity = sync_limiter.get_resource_capacity("api", "rpm", parents_only=True)
        assert len(parent_capacity.entities) == 2
        assert parent_capacity.total_capacity == 200
        parent_ids = {e.entity_id for e in parent_capacity.entities}
        assert parent_ids == {"org-1", "org-2"}
        assert "team-1" not in parent_ids

    def test_get_resource_capacity_utilization_calculation(self, sync_limiter):
        """Should calculate utilization percentage correctly."""
        sync_limiter.create_entity("entity-1")
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire("entity-1", "api", {"rpm": 30}, limits=limits):
            pass
        capacity = sync_limiter.get_resource_capacity("api", "rpm")
        assert len(capacity.entities) == 1
        entity = capacity.entities[0]
        assert entity.available == 70
        assert entity.capacity == 100
        assert abs(entity.utilization_pct - 30.0) < 0.1

    def test_get_resource_capacity_empty_result(self, sync_limiter):
        """Should return empty capacity when no buckets match."""
        capacity = sync_limiter.get_resource_capacity("nonexistent-resource", "rpm")
        assert capacity.resource == "nonexistent-resource"
        assert capacity.limit_name == "rpm"
        assert capacity.total_capacity == 0
        assert capacity.total_available == 0
        assert len(capacity.entities) == 0
        assert capacity.utilization_pct == 0.0

    def test_get_resource_capacity_sharded_entity_deduplication(self, sync_limiter):
        """Sharded entities should report capacity once, not per-shard."""
        from zae_limiter import schema

        sync_limiter.create_entity("sharded-user")
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire("sharded-user", "gpt-4", {"rpm": 10}, limits=limits):
            pass
        repo = sync_limiter._repository
        client = repo._get_client()
        shard0_key = {
            "PK": {"S": schema.pk_bucket(repo._namespace_id, "sharded-user", "gpt-4", 0)},
            "SK": {"S": schema.sk_state()},
        }
        shard0_resp = client.get_item(TableName=repo.table_name, Key=shard0_key)
        shard0_item = shard0_resp["Item"]
        shard1_item = dict(shard0_item)
        shard1_item["PK"] = {"S": schema.pk_bucket(repo._namespace_id, "sharded-user", "gpt-4", 1)}
        shard1_item["GSI2SK"] = {"S": schema.gsi2_sk_bucket("sharded-user", 1)}
        shard1_item["GSI3SK"] = {"S": schema.gsi3_sk_bucket("gpt-4", 1)}
        shard1_item["shard_count"] = {"N": "2"}
        client.update_item(
            TableName=repo.table_name,
            Key=shard0_key,
            UpdateExpression="SET shard_count = :sc",
            ExpressionAttributeValues={":sc": {"N": "2"}},
        )
        client.put_item(TableName=repo.table_name, Item=shard1_item)
        capacity = sync_limiter.get_resource_capacity("gpt-4", "rpm")
        assert capacity.total_capacity == 100
        assert len(capacity.entities) == 1
        assert capacity.entities[0].entity_id == "sharded-user"
        assert capacity.entities[0].capacity == 100


class TestRateLimiterCapacityEdgeCases:
    """Tests for edge cases in capacity calculations."""

    def test_time_until_available_skips_zero_amount(self, sync_limiter):
        """time_until_available should skip limits with zero needed amount."""
        limits = [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 10000)]
        with sync_limiter.acquire(
            entity_id="key-1", resource="gpt-4", limits=limits, consume={"rpm": 100, "tpm": 100}
        ):
            pass
        wait = sync_limiter.time_until_available(
            entity_id="key-1", resource="gpt-4", limits=limits, needed={"rpm": 50, "tpm": 0}
        )
        assert 29 < wait < 31


class TestListResourcesWithEntityConfigs:
    """Tests for listing resources with entity-level custom limits."""

    def test_list_resources_with_entity_configs(self, sync_limiter):
        """Should list resources that have entity-level custom limits."""
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="gpt-4")
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 200)], resource="claude-3")
        resources = sync_limiter.list_resources_with_entity_configs()
        assert "gpt-4" in resources
        assert "claude-3" in resources

    def test_list_resources_with_entity_configs_empty(self, sync_limiter):
        """Should return empty list when no entity configs exist."""
        resources = sync_limiter.list_resources_with_entity_configs()
        assert resources == []


class TestRateLimiterFetchBucketsFallback:
    """Tests for _fetch_buckets fallback path when batch not supported."""

    def test_fetch_buckets_fallback_fresh_entity(self, sync_limiter, monkeypatch):
        """Fallback path should work for entities without existing buckets."""
        from zae_limiter.models import BackendCapabilities

        limits = [Limit.per_minute("rpm", 100)]
        no_batch_capabilities = BackendCapabilities(
            supports_audit_logging=True,
            supports_usage_snapshots=True,
            supports_infrastructure_management=True,
            supports_change_streams=True,
            supports_batch_operations=False,
        )
        monkeypatch.setattr(sync_limiter._repository, "_capabilities", no_batch_capabilities)
        with sync_limiter.acquire(
            entity_id="fresh-entity", resource="gpt-4", limits=limits, consume={"rpm": 1}
        ) as lease:
            assert lease.consumed == {"rpm": 1}


class TestRateLimiterInputValidation:
    def test_acquire_validates_entity_id(self, sync_limiter):
        """Acquire should reject entity_id containing reserved delimiter."""
        limits = [Limit.per_minute("rpm", 100)]
        with pytest.raises(InvalidIdentifierError) as exc_info:
            with sync_limiter.acquire("user#123", "api", {"rpm": 1}, limits=limits):
                pass
        assert exc_info.value.field == "entity_id"
        assert "#" in exc_info.value.reason

    def test_acquire_validates_resource(self, sync_limiter):
        """Acquire should reject resource containing reserved delimiter."""
        limits = [Limit.per_minute("rpm", 100)]
        with pytest.raises(InvalidNameError) as exc_info:
            with sync_limiter.acquire("user-123", "api#v2", {"rpm": 1}, limits=limits):
                pass
        assert exc_info.value.field == "resource"
        assert "#" in exc_info.value.reason

    def test_acquire_validates_empty_entity_id(self, sync_limiter):
        """Acquire should reject empty entity_id."""
        limits = [Limit.per_minute("rpm", 100)]
        with pytest.raises(InvalidIdentifierError) as exc_info:
            with sync_limiter.acquire("", "api", {"rpm": 1}, limits=limits):
                pass
        assert exc_info.value.field == "entity_id"
        assert "empty" in exc_info.value.reason

    def test_acquire_validates_empty_resource(self, sync_limiter):
        """Acquire should reject empty resource."""
        limits = [Limit.per_minute("rpm", 100)]
        with pytest.raises(InvalidNameError) as exc_info:
            with sync_limiter.acquire("user-123", "", {"rpm": 1}, limits=limits):
                pass
        assert exc_info.value.field == "resource"
        assert "empty" in exc_info.value.reason

    def test_acquire_accepts_valid_inputs(self, sync_limiter):
        """Acquire should accept valid entity_id and resource."""
        limits = [Limit.per_minute("rpm", 100)]
        with sync_limiter.acquire("user-123", "gpt-3.5-turbo", {"rpm": 1}, limits=limits):
            pass


class TestRateLimiterIsAvailable:
    """Tests for is_available() health check method."""

    def test_is_available_returns_true_when_table_exists(self, sync_limiter):
        """is_available should return True when DynamoDB table is reachable."""
        result = sync_limiter.is_available()
        assert result is True

    def test_is_available_returns_false_on_client_error(self, sync_limiter, monkeypatch):
        """is_available should return False when DynamoDB returns error."""

        def mock_error(*args, **kwargs):
            raise ClientError(
                {"Error": {"Code": "ResourceNotFoundException", "Message": "Table not found"}},
                "GetItem",
            )

        monkeypatch.setattr(sync_limiter._repository, "ping", mock_error)
        result = sync_limiter.is_available()
        assert result is False

    def test_is_available_returns_false_on_connection_error(self, sync_limiter, monkeypatch):
        """is_available should return False when connection fails."""

        def mock_connection_error(*args, **kwargs):
            raise ConnectionError("Cannot connect to DynamoDB")

        monkeypatch.setattr(sync_limiter._repository, "ping", mock_connection_error)
        result = sync_limiter.is_available()
        assert result is False

    def test_is_available_custom_timeout(self, sync_limiter):
        """is_available should respect custom timeout parameter."""
        result = sync_limiter.is_available(timeout=5.0)
        assert result is True


class TestRateLimiterAudit:
    """Tests for audit functionality."""

    def test_get_audit_events_after_create_entity(self, sync_limiter):
        """Test that create_entity logs an audit event."""
        sync_limiter.create_entity(
            entity_id="proj-1", name="Test Project", principal="admin@example.com"
        )
        events = sync_limiter.get_audit_events("proj-1")
        assert len(events) == 1
        assert events[0].action == "entity_created"
        assert events[0].entity_id == "proj-1"
        assert events[0].principal == "admin@example.com"
        assert events[0].details["name"] == "Test Project"

    def test_get_audit_events_after_delete_entity(self, sync_limiter):
        """Test that delete_entity logs an audit event."""
        sync_limiter.create_entity(entity_id="proj-1", principal="admin")
        sync_limiter.delete_entity("proj-1", principal="admin")
        events = sync_limiter.get_audit_events("proj-1")
        assert len(events) == 2
        assert events[0].action == "entity_deleted"
        assert events[1].action == "entity_created"

    def test_get_audit_events_after_set_limits(self, sync_limiter):
        """Test that set_limits logs an audit event."""
        sync_limiter.create_entity(entity_id="proj-1")
        limits = [Limit.per_minute("rpm", 100)]
        sync_limiter.set_limits("proj-1", limits, principal="admin")
        events = sync_limiter.get_audit_events("proj-1")
        limit_events = [e for e in events if e.action == "limits_set"]
        assert len(limit_events) == 1
        assert limit_events[0].principal == "admin"

    def test_get_audit_events_after_delete_limits(self, sync_limiter):
        """Test that delete_limits logs an audit event."""
        sync_limiter.create_entity(entity_id="proj-1")
        limits = [Limit.per_minute("rpm", 100)]
        sync_limiter.set_limits("proj-1", limits)
        sync_limiter.delete_limits("proj-1", principal="admin")
        events = sync_limiter.get_audit_events("proj-1")
        delete_events = [e for e in events if e.action == "limits_deleted"]
        assert len(delete_events) == 1
        assert delete_events[0].principal == "admin"

    def test_get_audit_events_with_limit(self, sync_limiter):
        """Test pagination limit parameter."""
        sync_limiter.create_entity(entity_id="proj-1")
        for i in range(5):
            sync_limiter.set_limits("proj-1", [Limit.per_minute("rpm", 100 + i)])
        events = sync_limiter.get_audit_events("proj-1", limit=3)
        assert len(events) == 3

    def test_get_audit_events_empty(self, sync_limiter):
        """Test getting events for entity with no events."""
        events = sync_limiter.get_audit_events("nonexistent")
        assert events == []

    def test_create_entity_without_principal_uses_auto_detection(self, sync_limiter):
        """Test that principal is auto-detected from AWS identity when not provided."""
        sync_limiter.create_entity(entity_id="proj-1")
        events = sync_limiter.get_audit_events("proj-1")
        assert len(events) == 1

    def test_explicit_principal_overrides_auto_detection(self, sync_limiter):
        """Test that explicit principal overrides auto-detection."""
        sync_limiter.create_entity(entity_id="proj-1", principal="explicit-user")
        events = sync_limiter.get_audit_events("proj-1")
        assert len(events) == 1
        assert events[0].principal == "explicit-user"


class TestRateLimiterUsageSnapshots:
    """Tests for usage snapshot queries."""

    @pytest.fixture
    def limiter_with_snapshots(self, sync_limiter):
        """Limiter with test usage snapshots."""
        from zae_limiter import schema

        repo = sync_limiter._repository
        client = repo._get_client()
        snapshots_data = [
            ("entity-1", "gpt-4", "hourly", "2024-01-15T10:00:00Z", {"tpm": 1000, "rpm": 5}),
            ("entity-1", "gpt-4", "hourly", "2024-01-15T11:00:00Z", {"tpm": 2000, "rpm": 10}),
            ("entity-1", "gpt-4", "daily", "2024-01-15T00:00:00Z", {"tpm": 3000, "rpm": 15}),
        ]
        for entity_id, resource, window_type, window_start, counters in snapshots_data:
            item = {
                "PK": {"S": schema.pk_entity(sync_limiter._repository.namespace_id, entity_id)},
                "SK": {"S": schema.sk_usage(resource, window_start)},
                "entity_id": {"S": entity_id},
                "resource": {"S": resource},
                "window": {"S": window_type},
                "window_start": {"S": window_start},
                "total_events": {"N": str(sum(counters.values()))},
                "GSI2PK": {
                    "S": schema.gsi2_pk_resource(sync_limiter._repository.namespace_id, resource)
                },
                "GSI2SK": {"S": f"USAGE#{window_start}#{entity_id}"},
            }
            for name, value in counters.items():
                item[name] = {"N": str(value)}
            client.put_item(TableName=repo.table_name, Item=item)
        yield sync_limiter

    def test_get_usage_snapshots_basic(self, limiter_with_snapshots):
        """Test basic snapshot query."""
        snapshots, next_key = limiter_with_snapshots.get_usage_snapshots(entity_id="entity-1")
        assert len(snapshots) == 3
        assert all(s.entity_id == "entity-1" for s in snapshots)
        assert next_key is None

    def test_get_usage_snapshots_with_datetime_conversion(self, limiter_with_snapshots):
        """Test datetime parameters are converted to ISO strings."""
        from datetime import datetime

        snapshots, _ = limiter_with_snapshots.get_usage_snapshots(
            entity_id="entity-1",
            start_time=datetime(2024, 1, 15, 10, 0, 0),
            end_time=datetime(2024, 1, 15, 11, 0, 0),
        )
        assert len(snapshots) == 2
        window_starts = {s.window_start for s in snapshots}
        assert "2024-01-15T10:00:00Z" in window_starts
        assert "2024-01-15T11:00:00Z" in window_starts

    def test_get_usage_snapshots_with_timezone_aware_datetime(self, limiter_with_snapshots):
        """Test timezone-aware datetime is converted to UTC."""
        from datetime import datetime

        eastern = UTC
        start = datetime(2024, 1, 15, 10, 0, 0, tzinfo=eastern)
        snapshots, _ = limiter_with_snapshots.get_usage_snapshots(
            entity_id="entity-1", start_time=start
        )
        assert len(snapshots) >= 1

    def test_get_usage_summary_basic(self, limiter_with_snapshots):
        """Test basic summary aggregation."""
        summary = limiter_with_snapshots.get_usage_summary(
            entity_id="entity-1", resource="gpt-4", window_type="hourly"
        )
        assert summary.snapshot_count == 2
        assert summary.total["tpm"] == 3000
        assert summary.total["rpm"] == 15

    def test_get_usage_summary_with_datetime(self, limiter_with_snapshots):
        """Test summary with datetime parameters."""
        from datetime import datetime

        summary = limiter_with_snapshots.get_usage_summary(
            entity_id="entity-1",
            start_time=datetime(2024, 1, 15, 10, 0, 0),
            end_time=datetime(2024, 1, 15, 10, 0, 0),
        )
        assert summary.snapshot_count == 1
        assert summary.total["tpm"] == 1000

    def test_get_usage_snapshots_requires_entity_or_resource(self, limiter_with_snapshots):
        """Should raise ValueError if neither entity_id nor resource provided."""
        with pytest.raises(ValueError, match="Either entity_id or resource"):
            limiter_with_snapshots.get_usage_snapshots()

    def test_get_usage_summary_requires_entity_or_resource(self, limiter_with_snapshots):
        """Should raise ValueError if neither entity_id nor resource provided."""
        with pytest.raises(ValueError, match="Either entity_id or resource"):
            limiter_with_snapshots.get_usage_summary()


class TestInfrastructureDiscovery:
    """Tests for SyncInfrastructureDiscovery class."""

    def test_list_limiters_empty(self):
        """list_limiters returns empty list when no managed stacks exist."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": []})
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert limiters == []

    def test_list_limiters_filters_by_tag_or_prefix(self):
        """list_limiters returns stacks with ManagedBy tag or ZAEL- prefix."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "my-tagged-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [
                                {"Key": "ManagedBy", "Value": "zae-limiter"},
                                {"Key": "zae-limiter:name", "Value": "my-tagged-app"},
                            ],
                        },
                        {
                            "StackName": "other-stack",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                        {
                            "StackName": "ZAEL-another",
                            "StackStatus": "UPDATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 14, 9, 0, 0),
                            "Tags": [],
                        },
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 2
            stack_names = {lim.stack_name for lim in limiters}
            assert "my-tagged-app" in stack_names
            assert "ZAEL-another" in stack_names
            assert "other-stack" not in stack_names

    def test_list_limiters_extracts_user_name_from_tag(self):
        """list_limiters extracts user_name from zae-limiter:name tag."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "my-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [
                                {"Key": "ManagedBy", "Value": "zae-limiter"},
                                {"Key": "zae-limiter:name", "Value": "my-app"},
                            ],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].stack_name == "my-app"
            assert limiters[0].user_name == "my-app"

    def test_list_limiters_extracts_user_name_from_legacy_prefix(self):
        """list_limiters strips ZAEL- prefix for user_name on legacy stacks."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-my-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].stack_name == "ZAEL-my-app"
            assert limiters[0].user_name == "my-app"

    def test_list_limiters_with_version_tags(self):
        """list_limiters extracts version info from tags."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "my-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [
                                {"Key": "ManagedBy", "Value": "zae-limiter"},
                                {"Key": "zae-limiter:name", "Value": "my-app"},
                                {"Key": "zae-limiter:version", "Value": "0.5.0"},
                                {"Key": "zae-limiter:lambda-version", "Value": "0.5.0"},
                                {"Key": "zae-limiter:schema-version", "Value": "1.0.0"},
                            ],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].version == "0.5.0"
            assert limiters[0].lambda_version == "0.5.0"
            assert limiters[0].schema_version == "1.0.0"

    def test_list_limiters_missing_tags(self):
        """list_limiters handles missing version tags gracefully."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-my-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].version is None
            assert limiters[0].lambda_version is None
            assert limiters[0].schema_version is None

    def test_list_limiters_handles_tagging_api_error(self):
        """list_limiters handles tagging API errors gracefully with describe_stacks fallback."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "my-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [
                                {"Key": "ManagedBy", "Value": "zae-limiter"},
                                {"Key": "zae-limiter:name", "Value": "my-app"},
                            ],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with patch.object(
                SyncInfrastructureDiscovery,
                "_discover_via_tagging_api",
                new_callable=MagicMock,
                return_value=[],
            ):
                with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                    limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].stack_name == "my-app"

    def test_list_limiters_with_last_updated_time(self):
        """list_limiters includes last_updated_time when present."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-my-app",
                            "StackStatus": "UPDATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "LastUpdatedTime": datetime(2024, 1, 16, 14, 0, 0),
                            "Tags": [],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].creation_time == "2024-01-15T10:30:00"
            assert limiters[0].last_updated_time == "2024-01-16T14:00:00"

    def test_list_limiters_various_statuses(self):
        """list_limiters correctly reports various stack statuses."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-healthy",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                        {
                            "StackName": "ZAEL-in-progress",
                            "StackStatus": "UPDATE_IN_PROGRESS",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                        {
                            "StackName": "ZAEL-failed",
                            "StackStatus": "CREATE_FAILED",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 3
            limiter_map = {lim.user_name: lim for lim in limiters}
            assert limiter_map["healthy"].is_healthy is True
            assert limiter_map["healthy"].is_in_progress is False
            assert limiter_map["healthy"].is_failed is False
            assert limiter_map["in-progress"].is_healthy is False
            assert limiter_map["in-progress"].is_in_progress is True
            assert limiter_map["in-progress"].is_failed is False
            assert limiter_map["failed"].is_healthy is False
            assert limiter_map["failed"].is_in_progress is False
            assert limiter_map["failed"].is_failed is True

    def test_list_limiters_sorted_by_user_name(self):
        """list_limiters returns results sorted by user_name."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-zebra",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                        {
                            "StackName": "ZAEL-apple",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                        {
                            "StackName": "ZAEL-banana",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            user_names = [lim.user_name for lim in limiters]
            assert user_names == ["apple", "banana", "zebra"]

    def test_list_limiters_pagination(self):
        """list_limiters handles pagination correctly."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=[
                    {
                        "Stacks": [
                            {
                                "StackName": "ZAEL-first",
                                "StackStatus": "CREATE_COMPLETE",
                                "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                                "Tags": [],
                            }
                        ],
                        "NextToken": "page2token",
                    },
                    {
                        "Stacks": [
                            {
                                "StackName": "ZAEL-second",
                                "StackStatus": "CREATE_COMPLETE",
                                "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                                "Tags": [],
                            }
                        ]
                    },
                ]
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 2
            user_names = {lim.user_name for lim in limiters}
            assert "first" in user_names
            assert "second" in user_names

    def test_list_limiters_includes_region(self):
        """list_limiters includes region in LimiterInfo."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-my-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="eu-west-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].region == "eu-west-1"

    def test_list_limiters_default_region(self):
        """list_limiters uses 'default' for region display when not specified."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-my-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        }
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region=None) as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].region == "default"

    def test_context_manager_cleanup(self):
        """Context manager properly cleans up resources."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": []})
            mock_client.__exit__ = MagicMock()
            mock_get_client.return_value = mock_client
            discovery = SyncInfrastructureDiscovery(region="us-east-1")
            with discovery:
                discovery.list_limiters()
            assert discovery._client is None
            assert discovery._session is None


class TestRateLimiterListDeployed:
    """Tests for SyncRateLimiter.list_deployed() class method."""

    def test_list_deployed_returns_limiter_info_list(self):
        """list_deployed returns a list of LimiterInfo objects."""
        mock_limiters = [
            LimiterInfo(
                stack_name="app1",
                user_name="app1",
                region="us-east-1",
                stack_status="CREATE_COMPLETE",
                creation_time="2024-01-15T10:30:00Z",
            ),
            LimiterInfo(
                stack_name="app2",
                user_name="app2",
                region="us-east-1",
                stack_status="UPDATE_COMPLETE",
                creation_time="2024-01-14T09:00:00Z",
            ),
        ]
        with patch(
            "zae_limiter.infra.sync_discovery.SyncInfrastructureDiscovery"
        ) as mock_discovery_class:
            mock_discovery = MagicMock()
            mock_discovery.list_limiters = MagicMock(return_value=mock_limiters)
            mock_discovery.__enter__ = MagicMock(return_value=mock_discovery)
            mock_discovery.__exit__ = MagicMock()
            mock_discovery_class.return_value = mock_discovery
            result = SyncRateLimiter.list_deployed(region="us-east-1")
            assert result == mock_limiters
            mock_discovery_class.assert_called_once_with(region="us-east-1", endpoint_url=None)

    def test_list_deployed_passes_endpoint_url(self):
        """list_deployed passes endpoint_url to SyncInfrastructureDiscovery."""
        with patch(
            "zae_limiter.infra.sync_discovery.SyncInfrastructureDiscovery"
        ) as mock_discovery_class:
            mock_discovery = MagicMock()
            mock_discovery.list_limiters = MagicMock(return_value=[])
            mock_discovery.__enter__ = MagicMock(return_value=mock_discovery)
            mock_discovery.__exit__ = MagicMock()
            mock_discovery_class.return_value = mock_discovery
            SyncRateLimiter.list_deployed(region="us-east-1", endpoint_url="http://localhost:4566")
            mock_discovery_class.assert_called_once_with(
                region="us-east-1", endpoint_url="http://localhost:4566"
            )

    def test_list_deployed_empty_result(self):
        """list_deployed returns empty list when no stacks exist."""
        with patch(
            "zae_limiter.infra.sync_discovery.SyncInfrastructureDiscovery"
        ) as mock_discovery_class:
            mock_discovery = MagicMock()
            mock_discovery.list_limiters = MagicMock(return_value=[])
            mock_discovery.__enter__ = MagicMock(return_value=mock_discovery)
            mock_discovery.__exit__ = MagicMock()
            mock_discovery_class.return_value = mock_discovery
            result = SyncRateLimiter.list_deployed(region="us-east-1")
            assert result == []

    def test_list_deployed_propagates_client_error(self):
        """list_deployed propagates ClientError from CloudFormation."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                side_effect=ClientError(
                    {"Error": {"Code": "AccessDenied", "Message": "Not authorized"}},
                    "DescribeStacks",
                )
            )
            mock_get_client.return_value = mock_client
            with pytest.raises(ClientError) as exc_info:
                SyncRateLimiter.list_deployed(region="us-east-1")
            assert "AccessDenied" in str(exc_info.value)

    def test_list_deployed_is_class_method(self):
        """list_deployed is a class method, not an instance method."""
        assert hasattr(SyncRateLimiter, "list_deployed")
        assert callable(SyncRateLimiter.list_deployed)
        with patch(
            "zae_limiter.infra.sync_discovery.SyncInfrastructureDiscovery"
        ) as mock_discovery_class:
            mock_discovery = MagicMock()
            mock_discovery.list_limiters = MagicMock(return_value=[])
            mock_discovery.__enter__ = MagicMock(return_value=mock_discovery)
            mock_discovery.__exit__ = MagicMock()
            mock_discovery_class.return_value = mock_discovery
            result = SyncRateLimiter.list_deployed(region="us-east-1")
            assert isinstance(result, list)

    def test_describe_stacks_excludes_delete_complete(self):
        """describe_stacks discovery excludes DELETE_COMPLETE stacks."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(
                return_value={
                    "Stacks": [
                        {
                            "StackName": "ZAEL-active-app",
                            "StackStatus": "CREATE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 15, 10, 30, 0),
                            "Tags": [],
                        },
                        {
                            "StackName": "ZAEL-deleted-app",
                            "StackStatus": "DELETE_COMPLETE",
                            "CreationTime": datetime(2024, 1, 14, 9, 0, 0),
                            "Tags": [],
                        },
                    ]
                }
            )
            mock_get_client.return_value = mock_client
            with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
                limiters = discovery.list_limiters()
            assert len(limiters) == 1
            assert limiters[0].stack_name == "ZAEL-active-app"

    def test_discovery_close_with_client_exception(self):
        """close() handles exceptions during client cleanup gracefully."""
        with patch.object(
            SyncInfrastructureDiscovery, "_get_client", new_callable=MagicMock
        ) as mock_get_client:
            mock_client = MagicMock()
            mock_client.describe_stacks = MagicMock(return_value={"Stacks": []})
            mock_client.__exit__ = MagicMock(side_effect=Exception("Cleanup failed"))
            mock_get_client.return_value = mock_client
            discovery = SyncInfrastructureDiscovery(region="us-east-1")
            with discovery:
                discovery.list_limiters()
            assert discovery._client is None
            assert discovery._session is None

    def test_discovery_close_without_client(self):
        """close() handles case when no client was ever created."""
        discovery = SyncInfrastructureDiscovery(region="us-east-1")
        discovery.close()
        assert discovery._client is None
        assert discovery._session is None

    def test_get_client_caches_client(self):
        """_get_client caches the client for subsequent calls."""
        with patch("zae_limiter.infra.sync_discovery.boto3") as mock_aioboto3:
            mock_session = MagicMock()
            mock_client = MagicMock()
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock()
            mock_session.client.return_value = mock_client
            mock_aioboto3.Session.return_value = mock_session
            discovery = SyncInfrastructureDiscovery(region="us-east-1")
            client1 = discovery._get_client()
            client2 = discovery._get_client()
            assert client1 is client2
            mock_aioboto3.Session.assert_called_once()
            discovery.close()

    def test_get_client_passes_region_and_endpoint(self):
        """_get_client passes region and endpoint_url to boto3."""
        with patch("zae_limiter.infra.sync_discovery.boto3") as mock_aioboto3:
            mock_session = MagicMock()
            mock_client = MagicMock()
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock()
            mock_session.client.return_value = mock_client
            mock_aioboto3.Session.return_value = mock_session
            discovery = SyncInfrastructureDiscovery(
                region="eu-west-1", endpoint_url="http://localhost:4566"
            )
            discovery._get_client()
            mock_session.client.assert_called_once_with(
                "cloudformation", region_name="eu-west-1", endpoint_url="http://localhost:4566"
            )
            discovery.close()

    def test_get_client_without_region_or_endpoint(self):
        """_get_client works without region or endpoint_url."""
        with patch("zae_limiter.infra.sync_discovery.boto3") as mock_aioboto3:
            mock_session = MagicMock()
            mock_client = MagicMock()
            mock_client.__enter__ = MagicMock(return_value=mock_client)
            mock_client.__exit__ = MagicMock()
            mock_session.client.return_value = mock_client
            mock_aioboto3.Session.return_value = mock_session
            discovery = SyncInfrastructureDiscovery()
            discovery._get_client()
            mock_session.client.assert_called_once_with("cloudformation")
            discovery.close()


class TestRateLimiterRepositoryParameter:
    """Tests for the new repository parameter in SyncRateLimiter constructor."""

    def test_repository_parameter_accepted(self, mock_dynamodb):
        """Test SyncRateLimiter accepts repository parameter."""
        from zae_limiter import SyncRepository

        repo = SyncRepository(name="my-repo-app", region="us-east-1")
        limiter = SyncRateLimiter(repository=repo)
        assert limiter._repository is repo
        assert limiter._repository.stack_name == "my-repo-app"
        limiter.close()

    def test_repository_parameter_conflict_with_name_raises(self, mock_dynamodb):
        """Test ValueError when both repository and name are provided."""
        from zae_limiter import SyncRepository

        repo = SyncRepository(name="my-app", region="us-east-1", _skip_deprecation_warning=True)
        with pytest.raises(ValueError) as exc_info:
            SyncRateLimiter(repository=repo, name="other-app")
        assert "Cannot specify both 'repository'" in str(exc_info.value)
        repo.close()

    def test_repository_parameter_conflict_with_region_raises(self, mock_dynamodb):
        """Test ValueError when both repository and region are provided."""
        from zae_limiter import SyncRepository

        repo = SyncRepository(name="my-app", region="us-east-1", _skip_deprecation_warning=True)
        with pytest.raises(ValueError) as exc_info:
            SyncRateLimiter(repository=repo, region="eu-west-1")
        assert "Cannot specify both 'repository'" in str(exc_info.value)
        repo.close()

    def test_repository_parameter_conflict_with_endpoint_url_raises(self, mock_dynamodb):
        """Test ValueError when both repository and endpoint_url are provided."""
        from zae_limiter import SyncRepository

        repo = SyncRepository(name="my-app", region="us-east-1", _skip_deprecation_warning=True)
        with pytest.raises(ValueError) as exc_info:
            SyncRateLimiter(repository=repo, endpoint_url="http://localhost:4566")
        assert "Cannot specify both 'repository'" in str(exc_info.value)
        repo.close()

    def test_repository_parameter_conflict_with_stack_options_raises(self, mock_dynamodb):
        """Test ValueError when both repository and stack_options are provided."""
        from zae_limiter import StackOptions, SyncRepository

        repo = SyncRepository(name="my-app", region="us-east-1", _skip_deprecation_warning=True)
        with pytest.raises(ValueError) as exc_info:
            SyncRateLimiter(repository=repo, stack_options=StackOptions())
        assert "Cannot specify both 'repository'" in str(exc_info.value)
        repo.close()

    def test_default_limiter_creates_repository_with_deprecation(self, mock_dynamodb):
        """Test SyncRateLimiter() with no args creates default repository but warns."""
        import warnings

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            limiter = SyncRateLimiter()
            deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
            assert len(deprecation_warnings) >= 1
            limiter_warnings = [
                x for x in deprecation_warnings if "without a repository" in str(x.message).lower()
            ]
            assert len(limiter_warnings) == 1
        assert limiter._repository.stack_name == "limiter"
        assert limiter._repository is not None
        limiter.close()


class TestDeprecatedConstructorParams:
    """Tests for individual deprecated constructor parameter warnings."""

    def test_on_unavailable_param_warns(self, mock_dynamodb):
        """Passing on_unavailable to constructor emits DeprecationWarning."""
        from zae_limiter.sync_limiter import OnUnavailable
        from zae_limiter.sync_repository import SyncRepository

        with pytest.warns(DeprecationWarning, match="on_unavailable"):
            limiter = SyncRateLimiter(name="test", on_unavailable=OnUnavailable.ALLOW)
        repo = limiter._repository
        assert isinstance(repo, SyncRepository)
        assert repo._on_unavailable_cache == "allow"
        limiter.close()

    def test_auto_update_param_warns(self, mock_dynamodb):
        """Passing auto_update to constructor emits DeprecationWarning."""
        with pytest.warns(DeprecationWarning, match="auto_update"):
            limiter = SyncRateLimiter(name="test", auto_update=True)
        limiter.close()

    def test_name_property_warns(self, mock_dynamodb):
        """Accessing SyncRateLimiter.name emits DeprecationWarning."""
        limiter = SyncRateLimiter(name="test")
        with pytest.warns(DeprecationWarning, match="SyncRateLimiter\\.name is deprecated"):
            name = limiter.name
        assert name == "test"
        limiter.close()

    def test_speculative_writes_does_not_warn(self, mock_dynamodb):
        """Passing speculative_writes does NOT emit DeprecationWarning."""
        import warnings

        from zae_limiter import SyncRepository

        repo = SyncRepository(name="test", region="us-east-1", _skip_deprecation_warning=True)
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            limiter = SyncRateLimiter(repository=repo, speculative_writes=False)
            deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)]
            assert len(deprecation_warnings) == 0
        assert limiter._speculative_writes is False
        limiter.close()

    def test_stack_name_property_warns(self, mock_dynamodb):
        """Accessing SyncRateLimiter.stack_name emits DeprecationWarning."""
        limiter = SyncRateLimiter(name="test")
        with pytest.warns(DeprecationWarning, match="stack_name"):
            name = limiter.stack_name
        assert name == "test"
        limiter.close()

    def test_table_name_property_warns(self, mock_dynamodb):
        """Accessing SyncRateLimiter.table_name emits DeprecationWarning."""
        limiter = SyncRateLimiter(name="test")
        with pytest.warns(DeprecationWarning, match="table_name"):
            name = limiter.table_name
        assert name == "test"
        limiter.close()


class TestRepositoryProtocolCompliance:
    """Tests that SyncRepository implements SyncRepositoryProtocol."""

    def test_repository_is_instance_of_protocol(self):
        """Test that SyncRepository passes isinstance check for SyncRepositoryProtocol."""
        from zae_limiter import SyncRepository, SyncRepositoryProtocol

        repo = SyncRepository(name="test", region="us-east-1", _skip_deprecation_warning=True)
        assert isinstance(repo, SyncRepositoryProtocol)

    def test_repository_protocol_is_runtime_checkable(self):
        """Test that SyncRepositoryProtocol is runtime checkable."""
        from zae_limiter import SyncRepositoryProtocol

        assert hasattr(SyncRepositoryProtocol, "__subclasshook__")

    def test_repository_has_capabilities_property(self):
        """Test that SyncRepository exposes capabilities."""
        from zae_limiter import BackendCapabilities, SyncRepository

        repo = SyncRepository(name="test", region="us-east-1", _skip_deprecation_warning=True)
        caps = repo.capabilities
        assert isinstance(caps, BackendCapabilities)
        assert caps.supports_audit_logging is True
        assert caps.supports_usage_snapshots is True
        assert caps.supports_infrastructure_management is True
        assert caps.supports_change_streams is True


class TestLazyImports:
    """Tests for lazy imports in __init__.py."""

    def test_repository_lazy_import(self):
        """Test SyncRepository can be imported from zae_limiter."""
        from zae_limiter import SyncRepository

        assert SyncRepository is not None
        assert SyncRepository.__name__ == "SyncRepository"

    def test_repository_protocol_lazy_import(self):
        """Test SyncRepositoryProtocol can be imported from zae_limiter."""
        from zae_limiter import SyncRepositoryProtocol

        assert SyncRepositoryProtocol is not None
        assert SyncRepositoryProtocol.__name__ == "SyncRepositoryProtocol"

    def test_stack_manager_lazy_import(self):
        """Test SyncStackManager can be imported from zae_limiter."""
        from zae_limiter import SyncStackManager

        assert SyncStackManager is not None
        assert SyncStackManager.__name__ == "SyncStackManager"

    def test_invalid_attribute_raises(self):
        """Test accessing invalid attribute raises AttributeError."""
        import zae_limiter

        with pytest.raises(AttributeError) as exc_info:
            _ = zae_limiter.NonExistentClass
        assert "has no attribute 'NonExistentClass'" in str(exc_info.value)


class TestRateLimiterConfigCache:
    """Tests for config cache management via SyncRepository (ADR-122)."""

    def test_get_cache_stats_returns_cache_stats(self, mock_dynamodb):
        """Test SyncRepository.get_cache_stats() returns CacheStats object."""
        limiter = SyncRateLimiter()
        stats = limiter._repository.get_cache_stats()
        assert isinstance(stats, CacheStats)
        assert stats.hits == 0
        assert stats.misses == 0
        assert stats.size == 0
        assert stats.ttl_seconds == 60
        limiter.close()

    def test_get_cache_stats_with_custom_ttl(self, mock_dynamodb):
        """Test SyncRepository.get_cache_stats() reflects custom TTL."""
        from zae_limiter import SyncRepository

        repo = SyncRepository(
            name="test-rate-limits",
            region="us-east-1",
            config_cache_ttl=120,
            _skip_deprecation_warning=True,
        )
        limiter = SyncRateLimiter(repository=repo)
        stats = limiter._repository.get_cache_stats()
        assert stats.ttl_seconds == 120
        limiter.close()

    def test_invalidate_config_cache(self, mock_dynamodb):
        """Test SyncRepository.invalidate_config_cache() clears cache entries."""
        from zae_limiter.sync_config_cache import CacheEntry

        limiter = SyncRateLimiter()
        entry = CacheEntry(value=[], expires_at=9999999999.0)
        limiter._repository._config_cache._resource_defaults["gpt-4"] = entry
        assert limiter._repository.get_cache_stats().size == 1
        limiter._repository.invalidate_config_cache()
        assert limiter._repository.get_cache_stats().size == 0
        limiter.close()

    def test_resolve_on_unavailable_uses_cache(self, sync_limiter):
        """resolve_on_unavailable() goes through config cache, not direct GetItem (#333)."""
        from zae_limiter import OnUnavailable

        sync_limiter.set_system_defaults(
            [Limit.per_minute("rpm", 100)], on_unavailable=OnUnavailable.ALLOW
        )
        result = sync_limiter._repository.resolve_on_unavailable()
        assert result == "allow"
        stats_after_first = sync_limiter._repository.get_cache_stats()
        assert stats_after_first.misses == 1
        result = sync_limiter._repository.resolve_on_unavailable()
        assert result == "allow"
        stats_after_second = sync_limiter._repository.get_cache_stats()
        assert stats_after_second.hits == 1
        assert stats_after_second.misses == 1


class TestResolveLinitsSequentialFallback:
    """Tests for SyncRepository._resolve_limits_sequential() (ADR-122)."""

    def test_sequential_entity_level(self, sync_limiter):
        """Sequential fallback returns entity-level config."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter.set_limits("user-seq", [Limit.per_minute("rpm", 500)], resource="api")
        repo = sync_limiter._repository
        limits, on_unavailable, source = repo._resolve_limits_sequential("user-seq", "api")
        assert source == "entity"
        assert limits is not None
        assert limits[0].capacity == 500
        assert on_unavailable is None

    def test_sequential_entity_default_level(self, sync_limiter):
        """Sequential fallback returns entity _default_ config."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter.set_limits("user-def", [Limit.per_minute("rpm", 300)], resource="_default_")
        repo = sync_limiter._repository
        limits, _, source = repo._resolve_limits_sequential("user-def", "api")
        assert source == "entity_default"
        assert limits is not None
        assert limits[0].capacity == 300

    def test_sequential_resource_level(self, sync_limiter):
        """Sequential fallback returns resource-level config."""
        sync_limiter.set_resource_defaults("api", [Limit.per_minute("rpm", 200)])
        repo = sync_limiter._repository
        limits, _, source = repo._resolve_limits_sequential("user-res", "api")
        assert source == "resource"
        assert limits is not None
        assert limits[0].capacity == 200

    def test_sequential_system_level(self, sync_limiter):
        """Sequential fallback returns system-level config."""
        from zae_limiter import OnUnavailable

        sync_limiter.set_system_defaults(
            [Limit.per_minute("rpm", 100)], on_unavailable=OnUnavailable.ALLOW
        )
        repo = sync_limiter._repository
        limits, on_unavailable, source = repo._resolve_limits_sequential("user-sys", "api")
        assert source == "system"
        assert limits is not None
        assert limits[0].capacity == 100
        assert on_unavailable == "allow"

    def test_sequential_no_config(self, sync_limiter):
        """Sequential fallback returns None when no config exists."""
        repo = sync_limiter._repository
        limits, on_unavailable, source = repo._resolve_limits_sequential("user-none", "api")
        assert limits is None
        assert source is None


class TestListEntitiesWithCustomLimits:
    """Tests for list_entities_with_custom_limits method."""

    def test_list_entities_with_custom_limits(self, sync_limiter):
        """list_entities_with_custom_limits returns entities with custom configs."""
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="gpt-4")
        sync_limiter.set_limits("user-2", [Limit.per_minute("rpm", 200)], resource="gpt-4")
        entities, cursor = sync_limiter.list_entities_with_custom_limits("gpt-4")
        assert set(entities) == {"user-1", "user-2"}
        assert cursor is None

    def test_list_entities_with_custom_limits_filters_by_resource(self, sync_limiter):
        """list_entities_with_custom_limits only returns entities for specified resource."""
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="gpt-4")
        sync_limiter.set_limits("user-2", [Limit.per_minute("rpm", 200)], resource="claude-3")
        entities, _ = sync_limiter.list_entities_with_custom_limits("gpt-4")
        assert set(entities) == {"user-1"}
        entities, _ = sync_limiter.list_entities_with_custom_limits("claude-3")
        assert set(entities) == {"user-2"}

    def test_list_entities_with_custom_limits_empty_result(self, sync_limiter):
        """Returns empty list when no entities have custom limits for resource."""
        entities, cursor = sync_limiter.list_entities_with_custom_limits("nonexistent")
        assert entities == []
        assert cursor is None


class TestConfigSourceTracking:
    """Tests for config source tracking (Issue #271: Refill-based TTL).

    _resolve_limits() should return (limits, config_source) tuple where
    config_source indicates which level the limits came from:
    - 'entity': Entity-level config
    - 'resource': Resource-level defaults
    - 'system': System-level defaults
    - 'override': Override parameter provided
    """

    def test_resolve_limits_returns_entity_source_when_entity_config_exists(self, sync_limiter):
        """_resolve_limits returns ('entity', limits) when entity has custom config."""
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="gpt-4")
        limits, source = sync_limiter._resolve_limits("user-1", "gpt-4", None)
        assert source == "entity"
        assert len(limits) == 1
        assert limits[0].name == "rpm"

    def test_resolve_limits_returns_resource_source_when_no_entity_config(self, sync_limiter):
        """_resolve_limits returns ('resource', limits) when using resource defaults."""
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 50)])
        limits, source = sync_limiter._resolve_limits("user-1", "gpt-4", None)
        assert source == "resource"
        assert len(limits) == 1
        assert limits[0].capacity == 50

    def test_resolve_limits_returns_system_source_when_no_resource_config(self, sync_limiter):
        """_resolve_limits returns ('system', limits) when using system defaults."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        limits, source = sync_limiter._resolve_limits("user-1", "gpt-4", None)
        assert source == "system"
        assert len(limits) == 1
        assert limits[0].capacity == 10

    def test_resolve_limits_returns_override_source_when_parameter_provided(self, sync_limiter):
        """_resolve_limits returns ('override', limits) when using parameter override."""
        override_limits = [Limit.per_minute("rpm", 200)]
        limits, source = sync_limiter._resolve_limits("user-1", "gpt-4", override_limits)
        assert source == "override"
        assert len(limits) == 1
        assert limits[0].capacity == 200

    def test_resolve_limits_entity_takes_precedence_over_resource(self, sync_limiter):
        """Entity-level config takes precedence over resource-level defaults."""
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 50)])
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="gpt-4")
        limits, source = sync_limiter._resolve_limits("user-1", "gpt-4", None)
        assert source == "entity"
        assert limits[0].capacity == 100

    def test_resolve_limits_resource_takes_precedence_over_system(self, sync_limiter):
        """Resource-level defaults take precedence over system-level defaults."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        sync_limiter.set_resource_defaults("gpt-4", [Limit.per_minute("rpm", 50)])
        limits, source = sync_limiter._resolve_limits("user-1", "gpt-4", None)
        assert source == "resource"
        assert limits[0].capacity == 50


class TestBatchedConfigResolutionFallback:
    """Tests for batched config resolution exception fallback (Issue #298)."""

    def test_resolve_limits_falls_back_on_batch_exception(self, sync_limiter):
        """When batch_get_configs raises, _resolve_limits falls back to sequential."""
        from unittest.mock import MagicMock, patch

        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with patch.object(
            sync_limiter._repository,
            "batch_get_configs",
            new=MagicMock(side_effect=RuntimeError("boom")),
        ):
            limits, source = sync_limiter._resolve_limits("user-1", "gpt-4", None)
        assert source == "system"
        assert len(limits) == 1
        assert limits[0].capacity == 1000


class TestBucketTTLConfiguration:
    """Tests for bucket_ttl_refill_multiplier on SyncRepository (Issue #271)."""

    def test_bucket_ttl_multiplier_default_is_seven(self, mock_dynamodb):
        """Default bucket_ttl_refill_multiplier on SyncRepository is 7."""
        limiter = SyncRateLimiter(name="test")
        assert limiter._repository._bucket_ttl_refill_multiplier == 7
        limiter.close()

    def test_bucket_ttl_multiplier_deprecated_param_warns(self, mock_dynamodb):
        """Passing bucket_ttl_refill_multiplier to constructor emits DeprecationWarning."""
        with pytest.warns(DeprecationWarning, match="bucket_ttl_refill_multiplier"):
            limiter = SyncRateLimiter(name="test", bucket_ttl_refill_multiplier=14)
        limiter.close()

    def test_bucket_ttl_multiplier_zero_disables(self, sync_limiter):
        """Setting bucket_ttl_refill_multiplier=0 on SyncRepository disables TTL."""
        sync_limiter._repository._bucket_ttl_refill_multiplier = 0
        assert sync_limiter._repository._bucket_ttl_refill_multiplier == 0


class TestLeaseEntryConfigTracking:
    """Tests for LeaseEntry config source tracking (Issue #271)."""

    def test_lease_entry_has_custom_config_field(self):
        """LeaseEntry has _has_custom_config field."""
        from zae_limiter.models import BucketState
        from zae_limiter.sync_lease import LeaseEntry

        state = BucketState(
            entity_id="test",
            resource="api",
            limit_name="rpm",
            tokens_milli=1000,
            capacity_milli=1000,
            refill_amount_milli=1000,
            refill_period_ms=60000,
            last_refill_ms=0,
        )
        entry = LeaseEntry(
            entity_id="test", resource="api", limit=Limit.per_minute("rpm", 100), state=state
        )
        assert entry._has_custom_config is False
        entry_with_custom = LeaseEntry(
            entity_id="test",
            resource="api",
            limit=Limit.per_minute("rpm", 100),
            state=state,
            _has_custom_config=True,
        )
        assert entry_with_custom._has_custom_config is True


class TestLeaseConfigPropagation:
    """Tests for SyncLease config source propagation (Issue #271)."""

    def test_lease_reads_ttl_multiplier_from_repository(self):
        """SyncLease reads bucket_ttl_refill_multiplier from repository."""
        from unittest.mock import MagicMock

        from zae_limiter.sync_lease import SyncLease

        repo = MagicMock()
        repo._bucket_ttl_refill_multiplier = 7
        lease = SyncLease(repository=repo)
        assert lease.repository._bucket_ttl_refill_multiplier == 7
        repo_custom = MagicMock()
        repo_custom._bucket_ttl_refill_multiplier = 14
        lease_custom = SyncLease(repository=repo_custom)
        assert lease_custom.repository._bucket_ttl_refill_multiplier == 14


class TestLeaseCommitTTL:
    """Tests for TTL behavior in SyncLease._commit() (Issue #271)."""

    def test_commit_sets_ttl_for_default_config(self, sync_limiter):
        """SyncLease._commit() sets TTL when using system/resource defaults."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 1}):
            pass
        buckets = sync_limiter._repository.get_buckets("user-1", "api")
        bucket = next((b for b in buckets if b.limit_name == "rpm"), None)
        assert bucket is not None
        from zae_limiter.schema import pk_bucket, sk_state

        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert "ttl" in item

    def test_get_item_returns_none_for_missing_bucket(self, sync_limiter):
        """_get_item returns None when bucket doesn't exist."""
        from zae_limiter.schema import pk_bucket, sk_state

        pk = pk_bucket(sync_limiter._repository.namespace_id, "nonexistent-user", "api", 0)
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is None

    def test_commit_removes_ttl_for_entity_config(self, sync_limiter):
        """SyncLease._commit() removes TTL when entity has custom limits."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 1}):
            pass
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 200)], resource="api")
        sync_limiter._repository.invalidate_config_cache()
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 1}):
            pass
        from zae_limiter.schema import pk_bucket, sk_state

        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert "ttl" not in item

    def test_commit_sets_ttl_for_entity_default_config(self, sync_limiter):
        """SyncLease._commit() sets TTL when entity has _default_ config (not resource-specific).

        Entity _default_ config is treated as a kind of default (not custom config),
        so TTL should be applied. This contrasts with resource-specific entity config
        which removes TTL.
        """
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire(entity_id="user-1", resource="gpt-4", consume={"rpm": 1}):
            pass
        from zae_limiter.schema import pk_bucket, sk_state

        pk = pk_bucket(sync_limiter._repository.namespace_id, "user-1", "gpt-4", 0)
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is not None
        assert "ttl" in item, "entity_default config should have TTL (treated as default)"

    def test_ttl_value_matches_formula(self, sync_limiter):
        """TTL = now + max_refill_period × multiplier."""
        import time

        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        now_before = int(time.time())
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 1}):
            pass
        now_after = int(time.time())
        from zae_limiter.schema import pk_bucket, sk_state

        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        expected_min = now_before + 60 * 7
        expected_max = now_after + 60 * 7 + 1
        ttl = item["ttl"]
        assert expected_min <= ttl <= expected_max

    def test_ttl_accounts_for_slow_refill_rate(self, sync_limiter):
        """TTL should be based on time to fill bucket, not just refill period (Issue #296).

        For a limit with capacity=1000 and refill_rate=10/min:
        - Time to fill bucket = 1000 / (10/min) = 100 minutes = 6000 seconds
        - TTL should be >= 6000 seconds × multiplier = 42000 seconds (700 minutes)
        """
        import time

        slow_refill_limit = Limit(
            name="tokens", capacity=1000, refill_amount=10, refill_period_seconds=60
        )
        sync_limiter.set_system_defaults([slow_refill_limit])
        now_before = int(time.time())
        with sync_limiter.acquire(entity_id="user-slow", resource="api", consume={"tokens": 1}):
            pass
        from zae_limiter.schema import pk_bucket, sk_state

        pk = pk_bucket(sync_limiter._repository.namespace_id, "user-slow", "api", 0)
        item = sync_limiter._repository._get_item(pk, sk_state())
        time_to_fill = 1000 / 10 * 60
        expected_min = now_before + int(time_to_fill * 7)
        ttl = item["ttl"]
        assert ttl >= expected_min, (
            f"TTL {ttl - now_before}s is shorter than time to fill bucket ({time_to_fill}s × 7 = {time_to_fill * 7}s)"
        )

    def test_ttl_disabled_when_multiplier_zero(self, mock_dynamodb):
        """No TTL when bucket_ttl_refill_multiplier=0."""
        from zae_limiter import SyncRepository

        repo = SyncRepository(name="test-no-ttl", region="us-east-1")
        repo._bucket_ttl_refill_multiplier = 0
        limiter = SyncRateLimiter(repository=repo)
        repo.create_table()
        with limiter:
            limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
            with limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 1}):
                pass
            from zae_limiter.schema import pk_bucket, sk_state

            pk = pk_bucket(limiter._repository.namespace_id, "user-1", "api", 0)
            item = limiter._repository._get_item(pk, sk_state())
            assert item is not None
            assert "ttl" not in item

    def test_commit_sets_ttl_after_deleting_entity_config(self, sync_limiter):
        """SyncLease._commit() sets TTL when entity downgrades from custom to default limits.

        When an entity's custom limits are deleted, the next acquire() should set TTL
        on the bucket since the entity now uses default limits again.
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 200)], resource="api")
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 1}):
            pass
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert "ttl" not in item
        sync_limiter.delete_limits("user-1", resource="api")
        sync_limiter._repository.invalidate_config_cache()
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 1}):
            pass
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert "ttl" in item


class TestBucketLimitSync:
    """Tests for bucket synchronization when limits are updated.

    These tests verify that bucket parameters (capacity, refill)
    are updated when entity limits change via set_limits().
    """

    def test_bucket_updated_when_limit_increased(self, sync_limiter):
        """Bucket capacity is synced when entity limit is increased.

        Behavior (issue #294):
        1. Create entity with rpm=100
        2. Use bucket (creates bucket with capacity=100)
        3. Update limit to rpm=200 - set_limits() syncs bucket
        4. Bucket capacity is now 200
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="api")
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 10}):
            pass
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 100000, "Initial capacity should be 100 RPM"
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 200)], resource="api")
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 200000, "Bucket capacity should be synced to 200 RPM"

    def test_bucket_updated_when_limit_decreased(self, sync_limiter):
        """Bucket capacity is synced when entity limit is decreased.

        Behavior (issue #294):
        1. Create entity with rpm=200
        2. Use bucket (creates bucket with capacity=200)
        3. Update limit to rpm=100 - set_limits() syncs bucket
        4. Bucket capacity is now 100
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 200)], resource="api")
        with sync_limiter.acquire(entity_id="user-1", resource="api", consume={"rpm": 10}):
            pass
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 200000, "Initial capacity should be 200 RPM"
        sync_limiter.set_limits("user-1", [Limit.per_minute("rpm", 100)], resource="api")
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-1", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 100000, "Bucket capacity should be synced to 100 RPM"

    def test_bucket_updated_with_multiple_limits(self, sync_limiter):
        """All bucket params synced when entity has multiple limits.

        Verifies that the SET expression correctly handles multiple limits
        and all parameters (capacity, refill) are updated.
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_limits(
            "user-2", [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 10000)], resource="api"
        )
        with sync_limiter.acquire(
            entity_id="user-2", resource="api", consume={"rpm": 1, "tpm": 10}
        ):
            pass
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-2", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 100000
        assert item["b_tpm_cp"] == 10000000
        sync_limiter.set_limits(
            "user-2", [Limit.per_minute("rpm", 200), Limit.per_minute("tpm", 20000)], resource="api"
        )
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-2", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 200000, "rpm capacity should be synced"
        assert item["b_tpm_cp"] == 20000000, "tpm capacity should be synced"

    def test_bucket_refill_params_synced_when_changed(self, sync_limiter):
        """Bucket refill rate is synced when limit period changes.

        Verifies that all bucket parameters are updated:
        capacity, refill_amount, refill_period.
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_limits("user-3", [Limit.per_minute("rpm", 100)], resource="api")
        with sync_limiter.acquire(entity_id="user-3", resource="api", consume={"rpm": 1}):
            pass
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-3", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 100000
        assert item["b_rpm_ra"] == 100000
        assert item["b_rpm_rp"] == 60000
        sync_limiter.set_limits("user-3", [Limit.per_hour("rpm", 200)], resource="api")
        item = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-3", "api", 0), sk_state()
        )
        assert item is not None
        assert item["b_rpm_cp"] == 200000, "capacity should be synced"
        assert item["b_rpm_ra"] == 200000, "refill_amount should be synced"
        assert item["b_rpm_rp"] == 3600000, "refill_period should be synced (3600s)"

    def test_set_limits_with_empty_list_skips_bucket_sync(self, sync_limiter):
        """set_limits() with empty limits list skips bucket sync.

        Verifies the early return path when limits=[] is passed.
        """
        sync_limiter.set_limits("user-4", [Limit.per_minute("rpm", 100)], resource="api")
        sync_limiter.set_limits("user-4", [], resource="api")

    def test_bucket_sync_skipped_when_bucket_does_not_exist(self, sync_limiter):
        """Bucket sync is skipped when bucket doesn't exist yet.

        Verifies ConditionalCheckFailedException is handled gracefully.
        """
        sync_limiter.set_limits("user-5", [Limit.per_minute("rpm", 100)], resource="api")
        sync_limiter.set_limits("user-5", [Limit.per_minute("rpm", 200)], resource="api")

    def test_bucket_sync_reraises_unexpected_client_error(self, sync_limiter):
        """Unexpected ClientError during bucket sync is re-raised.

        Verifies that non-ConditionalCheckFailed errors propagate.
        """
        sync_limiter.set_limits("user-6", [Limit.per_minute("rpm", 100)], resource="api")
        with sync_limiter.acquire(entity_id="user-6", resource="api", consume={"rpm": 1}):
            pass
        original_update = sync_limiter._repository._client.update_item

        def mock_update_item(**kwargs):
            if kwargs.get("Key", {}).get("SK", {}).get("S", "") == "#STATE":
                raise ClientError(
                    {"Error": {"Code": "InternalServerError", "Message": "Test error"}},
                    "UpdateItem",
                )
            return original_update(**kwargs)

        sync_limiter._repository._client.update_item = mock_update_item
        with pytest.raises(ClientError) as exc_info:
            sync_limiter.set_limits("user-6", [Limit.per_minute("rpm", 200)], resource="api")
        assert exc_info.value.response["Error"]["Code"] == "InternalServerError"


class TestBucketReconciliation:
    """Tests for eager bucket reconciliation on config changes (issue #327).

    Verifies that set_limits() removes TTL, delete_limits() syncs bucket to
    effective defaults, and stale limit attributes are removed.
    """

    def test_set_limits_removes_ttl_from_bucket(self, sync_limiter):
        """set_limits() removes TTL from existing bucket that had TTL.

        Transition: system defaults (TTL) → entity config (no TTL).
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire(entity_id="user-ttl", resource="api", consume={"rpm": 1}):
            pass
        pk = pk_bucket(sync_limiter._repository.namespace_id, "user-ttl", "api", 0)
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is not None
        assert "ttl" in item
        sync_limiter.set_limits("user-ttl", [Limit.per_minute("rpm", 200)], resource="api")
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is not None
        assert "ttl" not in item
        assert item["b_rpm_cp"] == 200000

    def test_delete_limits_sets_ttl_and_syncs_to_defaults(self, sync_limiter):
        """delete_limits() syncs bucket to resource defaults with TTL.

        Transition: entity config (no TTL) → resource defaults (TTL).
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_resource_defaults("api", [Limit.per_minute("rpm", 100)])
        sync_limiter.set_limits("user-del", [Limit.per_minute("rpm", 500)], resource="api")
        sync_limiter._repository.invalidate_config_cache()
        with sync_limiter.acquire(entity_id="user-del", resource="api", consume={"rpm": 1}):
            pass
        pk = pk_bucket(sync_limiter._repository.namespace_id, "user-del", "api", 0)
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is not None
        assert "ttl" not in item
        assert item["b_rpm_cp"] == 500000
        sync_limiter.delete_limits("user-del", resource="api")
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is not None
        assert "ttl" in item, "TTL should be set (now using defaults)"
        assert item["b_rpm_cp"] == 100000, "Capacity should match resource defaults"

    def test_delete_limits_removes_stale_attributes(self, sync_limiter):
        """delete_limits() removes stale limit attributes from bucket.

        Entity had [rpm, tpm], defaults have [rpm] only — tpm attrs removed.
        """
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter.set_limits(
            "user-stale",
            [Limit.per_minute("rpm", 500), Limit.per_minute("tpm", 50000)],
            resource="api",
        )
        sync_limiter._repository.invalidate_config_cache()
        with sync_limiter.acquire(
            entity_id="user-stale", resource="api", consume={"rpm": 1, "tpm": 100}
        ):
            pass
        pk = pk_bucket(sync_limiter._repository.namespace_id, "user-stale", "api", 0)
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is not None
        assert "b_tpm_cp" in item, "tpm limit should exist in bucket"
        sync_limiter.delete_limits("user-stale", resource="api")
        item = sync_limiter._repository._get_item(pk, sk_state())
        assert item is not None
        assert "b_rpm_cp" in item, "rpm limit should still exist"
        assert item["b_rpm_cp"] == 100000, "rpm should match system defaults"
        assert "b_tpm_cp" not in item, "Stale tpm limit should be removed"
        assert "b_tpm_tk" not in item, "Stale tpm tokens should be removed"
        assert "b_tpm_tc" not in item, "Stale tpm counter should be removed"

    def test_delete_limits_no_effective_defaults(self, sync_limiter):
        """delete_limits() leaves bucket as-is when no fallback config exists."""
        from zae_limiter.schema import pk_bucket, sk_state

        sync_limiter.set_limits("user-orphan", [Limit.per_minute("rpm", 100)], resource="api")
        sync_limiter._repository.invalidate_config_cache()
        with sync_limiter.acquire(entity_id="user-orphan", resource="api", consume={"rpm": 1}):
            pass
        item_before = sync_limiter._repository._get_item(
            pk_bucket(sync_limiter._repository.namespace_id, "user-orphan", "api", 0), sk_state()
        )
        assert item_before is not None
        sync_limiter.delete_limits("user-orphan", resource="api")
        pk = pk_bucket(sync_limiter._repository.namespace_id, "user-orphan", "api", 0)
        item_after = sync_limiter._repository._get_item(pk, sk_state())
        assert item_after is not None
        assert item_after["b_rpm_cp"] == item_before["b_rpm_cp"]

    def test_delete_limits_bucket_does_not_exist(self, sync_limiter):
        """delete_limits() does not error when bucket doesn't exist."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter.set_limits("user-nobucket", [Limit.per_minute("rpm", 200)], resource="api")
        sync_limiter.delete_limits("user-nobucket", resource="api")

    def test_set_limits_evicts_config_cache(self, sync_limiter):
        """set_limits() evicts entity from config cache."""
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire(entity_id="user-cache", resource="api", consume={"rpm": 1}):
            pass
        sync_limiter.set_limits("user-cache", [Limit.per_minute("rpm", 200)], resource="api")
        assert ("user-cache", "api") not in sync_limiter._repository._config_cache._entity_limits

    def test_delete_limits_evicts_config_cache(self, sync_limiter):
        """delete_limits() evicts stale entity config from cache."""
        from zae_limiter.sync_config_cache import _NO_CONFIG

        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter.set_limits("user-dcache", [Limit.per_minute("rpm", 200)], resource="api")
        sync_limiter._repository.invalidate_config_cache()
        with sync_limiter.acquire(entity_id="user-dcache", resource="api", consume={"rpm": 1}):
            pass
        ns_id = sync_limiter._repository.namespace_id
        cache_key = (ns_id, "user-dcache", "api")
        entry = sync_limiter._repository._config_cache._entity_limits.get(cache_key)
        assert entry is not None and entry.value is not _NO_CONFIG
        sync_limiter.delete_limits("user-dcache", resource="api")
        entry = sync_limiter._repository._config_cache._entity_limits.get(cache_key)
        assert entry is None or entry.value is _NO_CONFIG, (
            "Cache should not contain stale entity limits after delete"
        )


class TestSpeculativeAcquire:
    """Tests for speculative UpdateItem fast path (Issue #315)."""

    def test_speculative_enabled_by_default(self, sync_limiter):
        """speculative_writes defaults to True."""
        assert sync_limiter._speculative_writes is True

    def test_speculative_success_non_cascade(self, sync_limiter):
        """Speculative write succeeds for non-cascade entity with sufficient tokens."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}) as lease:
            assert lease._initial_committed is True
            assert len(lease.entries) > 0
            assert lease.entries[0].consumed == 1
            assert lease.entries[0]._initial_consumed == 1

    def test_speculative_fallback_on_missing_bucket(self, sync_limiter):
        """Falls back to slow path when bucket doesn't exist."""
        sync_limiter.create_entity("entity-new")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("entity-new", "gpt-4", {"rpm": 1}) as lease:
            assert len(lease.entries) > 0

    def test_speculative_fast_rejection(self, sync_limiter):
        """Raises RateLimitExceeded immediately when refill won't help."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with pytest.raises(RateLimitExceeded) as exc_info:
            with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
                pass
        assert len(exc_info.value.violations) >= 1

    def test_speculative_fallback_when_refill_helps(self, sync_limiter):
        """Falls back to slow path when refill would provide enough tokens."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 999}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}) as lease:
            assert len(lease.entries) > 0

    def test_speculative_with_multi_limit(self, sync_limiter):
        """Speculative works with multiple limits."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults(
            [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 200000)]
        )
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1, "tpm": 100}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1, "tpm": 100}) as lease:
            assert len(lease.entries) > 0

    def test_speculative_rollback_on_exception(self, sync_limiter):
        """Speculative lease rolls back on exception."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True

        def acquire_and_raise(lim):
            with lim.acquire("entity-1", "gpt-4", {"rpm": 10}):
                raise ValueError("test error")

        with pytest.raises(ValueError):
            acquire_and_raise(sync_limiter)
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass

    def test_speculative_cascade_both_succeed(self, sync_limiter):
        """Speculative cascade succeeds for both child and parent."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
            entity_ids = {e.entity_id for e in lease.entries}
            assert "child-1" in entity_ids
            assert "parent-1" in entity_ids

    def test_speculative_adjust_after_speculative(self, sync_limiter):
        """Adjustments work correctly after speculative commit."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 10}) as lease:
            lease.adjust(rpm=-5)
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass

    def test_speculative_config_changed_fallback(self, sync_limiter):
        """Falls back to slow path when limit is missing from bucket (config change)."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults(
            [Limit.per_minute("rpm", 1000), Limit.per_minute("tpm", 100000)]
        )
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1, "tpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        old_bucket_rpm_only = BucketState(
            entity_id="entity-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=50000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(success=False, old_buckets=[old_bucket_rpm_only])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1, "tpm": 1}) as lease:
                assert len(lease.entries) > 0
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_fails_compensate_and_fallback(self, sync_limiter):
        """Cascade: parent fails, child compensated, falls back to slow path."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_bucket = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=500000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_missing_fallback(self, sync_limiter):
        """Cascade: parent bucket missing, compensate child, slow path."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=None)
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert len(lease.entries) > 0
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_config_changed(self, sync_limiter):
        """Cascade: parent config changed (limit missing), compensate child, slow path."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_bucket_wrong_limit = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="tpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket_wrong_limit])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert len(lease.entries) > 0
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_exhausted_raises(self, sync_limiter):
        """Cascade: parent exhausted (refill won't help), raises RateLimitExceeded."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=9000,
            last_refill_ms=now_ms,
            capacity_milli=10000,
            refill_amount_milli=10000,
            refill_period_ms=60000,
        )
        parent_bucket_exhausted = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms,
            capacity_milli=10000,
            refill_amount_milli=10000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket_exhausted])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with pytest.raises(RateLimitExceeded) as exc_info:
                with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 10}):
                    pass
            assert len(exc_info.value.violations) >= 1
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_refill_helps_no_compensate(self, sync_limiter):
        """Cascade: parent fails with refill-would-help, parent-only slow path succeeds.

        Child stays consumed (no compensation). Parent is acquired via slow path.
        Saves 1 WCU (no compensation) + uses single-item write for parent only.
        """
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_bucket = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        original_write_each = sync_limiter._repository.write_each
        call_count = 0
        child_compensated = False

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        def mock_write_each(items):
            nonlocal child_compensated
            for item in items:
                key = item.get("Update", {}).get("Key", {})
                pk = key.get("PK", {}).get("S", "")
                if "child-1" in pk:
                    child_compensated = True
            return original_write_each(items)

        sync_limiter._repository.speculative_consume = mock_speculative
        sync_limiter._repository.write_each = mock_write_each
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
            assert not child_compensated, (
                "Child should not be compensated when parent slow path succeeds"
            )
        finally:
            sync_limiter._repository.speculative_consume = original_speculative
            sync_limiter._repository.write_each = original_write_each

    def test_speculative_cascade_parent_slow_path_fails_compensates(self, sync_limiter):
        """Cascade: parent-only slow path fails → compensate child → full slow path.

        ALL_OLD says refill WOULD help, but actual DDB parent is drained
        (concurrent consumer). Parent-only try_consume fails, returns None,
        child is compensated, then full _do_acquire also fails.
        """
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=9000,
            last_refill_ms=now_ms,
            capacity_milli=10000,
            refill_amount_milli=10000,
            refill_period_ms=60000,
        )
        parent_bucket_old = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms - 60000,
            capacity_milli=10000,
            refill_amount_milli=10000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket_old])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        with sync_limiter.acquire("parent-1", "gpt-4", {"rpm": 9}):
            pass
        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with pytest.raises(RateLimitExceeded):
                with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 10}):
                    pass
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_only_bucket_missing(self, sync_limiter):
        """Parent-only slow path returns None when parent bucket is missing.

        Covers line 975: parent bucket missing for a limit → return None →
        compensate child → full slow path.
        """
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_bucket_old = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms - 60000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        original_fetch = sync_limiter._fetch_buckets
        call_count = 0
        fetch_call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket_old])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        def mock_fetch_buckets(entity_ids, resource):
            nonlocal fetch_call_count
            fetch_call_count += 1
            if fetch_call_count == 1:
                return {}
            return original_fetch(entity_ids, resource)

        sync_limiter._repository.speculative_consume = mock_speculative
        sync_limiter._fetch_buckets = mock_fetch_buckets
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
        finally:
            sync_limiter._repository.speculative_consume = original_speculative
            sync_limiter._fetch_buckets = original_fetch

    def test_speculative_cascade_parent_only_commit_fails(self, sync_limiter):
        """Parent-only slow path: _commit_initial raises → return None.

        Covers lines 1037-1038: RateLimitExceeded from _commit_initial during
        parent write (concurrent contention).
        """
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_bucket_old = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms - 60000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        original_transact = sync_limiter._repository.transact_write
        call_count = 0
        transact_call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket_old])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        def mock_transact_write(items):
            nonlocal transact_call_count
            transact_call_count += 1
            if transact_call_count <= 2:
                from botocore.exceptions import ClientError

                raise ClientError(
                    {
                        "Error": {
                            "Code": "ConditionalCheckFailedException",
                            "Message": "Condition not met",
                        }
                    },
                    "TransactWriteItems",
                )
            return original_transact(items)

        sync_limiter._repository.speculative_consume = mock_speculative
        sync_limiter._repository.transact_write = mock_transact_write
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
        finally:
            sync_limiter._repository.speculative_consume = original_speculative
            sync_limiter._repository.transact_write = original_transact

    def test_speculative_skips_zero_consume_entries(self, sync_limiter):
        """Speculative path skips bucket entries with zero consume."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults(
            [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 200000)]
        )
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1, "tpm": 100}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        rpm_bucket = BucketState(
            entity_id="entity-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=99000,
            last_refill_ms=now_ms,
            capacity_milli=100000,
            refill_amount_milli=100000,
            refill_period_ms=60000,
        )
        tpm_bucket = BucketState(
            entity_id="entity-1",
            resource="gpt-4",
            limit_name="tpm",
            tokens_milli=199900000,
            last_refill_ms=now_ms,
            capacity_milli=200000000,
            refill_amount_milli=200000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            return SpeculativeResult(
                success=True, buckets=[rpm_bucket, tpm_bucket], cascade=False, parent_id=None
            )

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}) as lease:
                limit_names = [e.limit.name for e in lease.entries]
                assert "rpm" in limit_names
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_skips_zero_consume(self, sync_limiter):
        """Cascade: parent entries with zero consume are skipped."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults(
            [Limit.per_minute("rpm", 100), Limit.per_minute("tpm", 200000)]
        )
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1, "tpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_rpm = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=99000,
            last_refill_ms=now_ms,
            capacity_milli=100000,
            refill_amount_milli=100000,
            refill_period_ms=60000,
        )
        parent_rpm = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=99000,
            last_refill_ms=now_ms,
            capacity_milli=100000,
            refill_amount_milli=100000,
            refill_period_ms=60000,
        )
        parent_tpm = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="tpm",
            tokens_milli=199000000,
            last_refill_ms=now_ms,
            capacity_milli=200000000,
            refill_amount_milli=200000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(
                    success=True, buckets=[child_rpm], cascade=True, parent_id="parent-1"
                )
            if call_count == 2:
                return SpeculativeResult(
                    success=True, buckets=[parent_rpm, parent_tpm], cascade=False, parent_id=None
                )
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
                parent_entries = [e for e in lease.entries if e.entity_id == "parent-1"]
                assert all(e.limit.name == "rpm" for e in parent_entries)
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_child_refill_helps_fallback(self, sync_limiter):
        """Falls back to slow path when child refill would satisfy the request."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        old_bucket = BucketState(
            entity_id="entity-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return SpeculativeResult(success=False, old_buckets=[old_bucket])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository.speculative_consume = mock_speculative
        try:
            with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}) as lease:
                assert len(lease.entries) > 0
        finally:
            sync_limiter._repository.speculative_consume = original_speculative

    def test_speculative_cascade_parent_error_compensates_child_tokens(self, sync_limiter):
        """Parent-only slow path error must restore child tokens in DynamoDB.

        Regression test: before the fix, _try_parent_only_acquire exceptions
        propagated without compensating the child's speculative consumption,
        permanently leaking tokens from the child bucket.

        This test verifies the actual DynamoDB bucket balance is restored.
        """
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        buckets_before = sync_limiter._fetch_buckets(["child-1"], "gpt-4")
        child_key = ("child-1", "gpt-4", "rpm")
        tokens_before = buckets_before[child_key].tokens_milli
        sync_limiter._speculative_writes = True
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=tokens_before - 10000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_bucket_old = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms - 60000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_speculative = sync_limiter._repository.speculative_consume
        original_fetch = sync_limiter._fetch_buckets
        spec_call_count = 0
        fetch_call_count = 0

        def mock_speculative(entity_id, resource, consume, ttl_seconds=None):
            nonlocal spec_call_count
            spec_call_count += 1
            if spec_call_count == 1:
                original_speculative(entity_id, resource, consume, ttl_seconds)
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if spec_call_count == 2:
                return SpeculativeResult(success=False, old_buckets=[parent_bucket_old])
            return original_speculative(entity_id, resource, consume, ttl_seconds)

        def mock_fetch_raising(entity_ids, resource):
            nonlocal fetch_call_count
            fetch_call_count += 1
            if fetch_call_count == 1 and "parent-1" in entity_ids:
                raise RuntimeError("DynamoDB service unavailable")
            return original_fetch(entity_ids, resource)

        sync_limiter._repository.speculative_consume = mock_speculative
        sync_limiter._fetch_buckets = mock_fetch_raising
        try:
            with pytest.raises(RateLimiterUnavailable, match="DynamoDB service unavailable"):
                with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 10}):
                    pass
            buckets_after = original_fetch(["child-1"], "gpt-4")
            tokens_after = buckets_after[child_key].tokens_milli
            assert tokens_after == tokens_before, (
                f"Child tokens leaked! Before={tokens_before}, after={tokens_after}. Expected compensation to restore tokens."
            )
        finally:
            sync_limiter._repository.speculative_consume = original_speculative
            sync_limiter._fetch_buckets = original_fetch


class TestCascadeEntityCache:
    """Tests for cascade entity cache and parallel speculative writes (Issue #318)."""

    def test_cache_populated_from_speculative_success(self, sync_limiter):
        """Entity cache populated after first speculative success."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        cache = sync_limiter._repository._entity_cache
        ns_id = sync_limiter._repository.namespace_id
        assert (ns_id, "entity-1") in cache
        cascade, parent_id, _shards = cache[ns_id, "entity-1"]
        assert cascade is False
        assert parent_id is None

    def test_cache_populated_from_slow_path(self, sync_limiter):
        """Entity cache populated from slow path entity metadata."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        sync_limiter._speculative_writes = False
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        cache = sync_limiter._repository._entity_cache
        ns_id = sync_limiter._repository.namespace_id
        assert (ns_id, "entity-1") in cache
        cascade, parent_id, _shards = cache[ns_id, "entity-1"]
        assert cascade is False
        assert parent_id is None

    def test_cache_populated_cascade_entity(self, sync_limiter):
        """Entity cache correctly stores cascade + parent_id."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        cache = sync_limiter._repository._entity_cache
        ns_id = sync_limiter._repository.namespace_id
        assert (ns_id, "child-1") in cache
        cascade, parent_id, _shards = cache[ns_id, "child-1"]
        assert cascade is True
        assert parent_id == "parent-1"

    def test_cache_hit_non_cascade_single_write(self, sync_limiter):
        """Cache hit with cascade=False issues single speculative write."""
        sync_limiter.create_entity("entity-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}):
            pass
        single_call_count = 0
        original_single = sync_limiter._repository._speculative_consume_single

        def counting_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            nonlocal single_call_count
            single_call_count += 1
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = counting_single
        try:
            with sync_limiter.acquire("entity-1", "gpt-4", {"rpm": 1}) as lease:
                assert lease._initial_committed is True
            assert single_call_count == 1
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_parallel_cascade_both_succeed(self, sync_limiter):
        """Cache hit with cascade=True issues parallel writes, both succeed."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        single_calls: list[str] = []
        original_single = sync_limiter._repository._speculative_consume_single

        def tracking_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            single_calls.append(entity_id)
            return original_single(entity_id, resource, consume, ttl_seconds, shard_id=shard_id)

        sync_limiter._repository._speculative_consume_single = tracking_single
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert lease._initial_committed is True
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
            assert "child-1" in single_calls
            assert "parent-1" in single_calls
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_parallel_child_fails_parent_succeeds_compensates_parent(self, sync_limiter):
        """Parallel: child fails, parent succeeds — parent compensated, fall back."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_bucket_exhausted = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=500000,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_bucket = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single
        compensated_entity_ids: list[str] = []
        original_compensate = sync_limiter._compensate_speculative

        def tracking_compensate(entity_id, resource, consume):
            compensated_entity_ids.append(entity_id)
            return original_compensate(entity_id, resource, consume)

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            if entity_id == "child-1":
                return SpeculativeResult(success=False, old_buckets=[child_bucket_exhausted])
            if entity_id == "parent-1":
                return SpeculativeResult(
                    success=True, buckets=[parent_bucket], cascade=False, parent_id=None
                )
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        sync_limiter._compensate_speculative = tracking_compensate
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert len(lease.entries) > 0
            assert "parent-1" in compensated_entity_ids
        finally:
            sync_limiter._repository._speculative_consume_single = original_single
            sync_limiter._compensate_speculative = original_compensate

    def test_parallel_parent_fails_child_succeeds_compensates_child(self, sync_limiter):
        """Parallel: parent fails, child succeeds — child compensated or parent-only slow path."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single
        call_count = 0

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            nonlocal call_count
            call_count += 1
            if entity_id == "child-1" and call_count <= 2:
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1" and call_count <= 2:
                return SpeculativeResult(success=False, old_buckets=None)
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_parallel_both_fail_no_compensation(self, sync_limiter):
        """Parallel: both fail — no compensation needed, falls back to slow path."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_old = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=500000,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_old = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=500000,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single
        call_count = 0

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            nonlocal call_count
            call_count += 1
            if entity_id == "child-1" and call_count <= 2:
                return SpeculativeResult(success=False, old_buckets=[child_old])
            if entity_id == "parent-1" and call_count <= 2:
                return SpeculativeResult(success=False, old_buckets=[parent_old])
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert len(lease.entries) > 0
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_parallel_parent_exhausted_compensates_child_raises(self, sync_limiter):
        """Parallel: parent exhausted (no refill help), child compensated, raises."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 10)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=9000,
            last_refill_ms=now_ms,
            capacity_milli=10000,
            refill_amount_milli=10000,
            refill_period_ms=60000,
        )
        parent_bucket_exhausted = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=0,
            last_refill_ms=now_ms,
            capacity_milli=10000,
            refill_amount_milli=10000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            if entity_id == "child-1":
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1":
                return SpeculativeResult(success=False, old_buckets=[parent_bucket_exhausted])
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        try:
            with pytest.raises(RateLimitExceeded):
                with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 10}):
                    pass
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_parallel_both_succeed_skips_zero_amount_buckets(self, sync_limiter):
        """Parallel both succeed: buckets not in consume dict are skipped."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_rpm = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        child_tpm = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="tpm",
            tokens_milli=50000000,
            last_refill_ms=now_ms,
            capacity_milli=100000000,
            refill_amount_milli=100000000,
            refill_period_ms=60000,
        )
        parent_rpm = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            if entity_id == "child-1":
                return SpeculativeResult(
                    success=True, buckets=[child_rpm, child_tpm], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1":
                return SpeculativeResult(
                    success=True, buckets=[parent_rpm], cascade=False, parent_id=None
                )
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert lease._initial_committed is True
                limit_names = {e.limit.name for e in lease.entries}
                assert "rpm" in limit_names
                assert "tpm" not in limit_names
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_parallel_parent_fails_refill_helps_skips_zero_amount(self, sync_limiter):
        """Parent fails with refill help: extra child buckets not in consume are skipped."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_rpm = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        child_tpm = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="tpm",
            tokens_milli=50000000,
            last_refill_ms=now_ms,
            capacity_milli=100000000,
            refill_amount_milli=100000000,
            refill_period_ms=60000,
        )
        parent_old_rpm = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=500000,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single
        call_count = 0

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            nonlocal call_count
            call_count += 1
            if entity_id == "child-1" and call_count <= 2:
                return SpeculativeResult(
                    success=True, buckets=[child_rpm, child_tpm], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1" and call_count <= 2:
                return SpeculativeResult(success=False, old_buckets=[parent_old_rpm])
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                entity_ids = {e.entity_id for e in lease.entries}
                assert "child-1" in entity_ids
                assert "parent-1" in entity_ids
                child_entries = [e for e in lease.entries if e.entity_id == "child-1"]
                child_limit_names = {e.limit.name for e in child_entries}
                assert "rpm" in child_limit_names
                assert "tpm" not in child_limit_names
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_parallel_both_succeed_parent_skips_zero_amount(self, sync_limiter):
        """Parallel both succeed: parent bucket with zero consume amount is skipped."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_rpm = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_rpm = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_tpm = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="tpm",
            tokens_milli=50000000,
            last_refill_ms=now_ms,
            capacity_milli=100000000,
            refill_amount_milli=100000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            if entity_id == "child-1":
                return SpeculativeResult(
                    success=True, buckets=[child_rpm], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1":
                return SpeculativeResult(
                    success=True, buckets=[parent_rpm, parent_tpm], cascade=False, parent_id=None
                )
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                parent_entries = [e for e in lease.entries if e.entity_id == "parent-1"]
                parent_limit_names = {e.limit.name for e in parent_entries}
                assert "rpm" in parent_limit_names
                assert "tpm" not in parent_limit_names
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_nested_parent_failure_missing_limit_names(self, sync_limiter):
        """Nested parent failure: old_buckets don't include all consume keys."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_old_tpm_only = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="tpm",
            tokens_milli=50000000,
            last_refill_ms=now_ms,
            capacity_milli=100000000,
            refill_amount_milli=100000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            if entity_id == "child-1":
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1":
                return SpeculativeResult(success=False, old_buckets=[parent_old_tpm_only])
            return original_single(entity_id, resource, consume, ttl_seconds)

        sync_limiter._repository._speculative_consume_single = mock_single
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert len(lease.entries) > 0
        finally:
            sync_limiter._repository._speculative_consume_single = original_single

    def test_nested_parent_only_acquire_exception_compensates(self, sync_limiter):
        """Nested parent failure: exception during parent-only acquire compensates child."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_old = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=500000,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single
        original_parent_acquire = sync_limiter._try_parent_only_acquire

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            if entity_id == "child-1":
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1":
                return SpeculativeResult(success=False, old_buckets=[parent_old])
            return original_single(entity_id, resource, consume, ttl_seconds)

        def mock_parent_acquire(*args, **kwargs):
            raise RuntimeError("simulated parent acquire failure")

        sync_limiter._repository._speculative_consume_single = mock_single
        sync_limiter._try_parent_only_acquire = mock_parent_acquire
        try:
            with pytest.raises(RateLimiterUnavailable):
                with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
                    pass
        finally:
            sync_limiter._repository._speculative_consume_single = original_single
            sync_limiter._try_parent_only_acquire = original_parent_acquire

    def test_nested_parent_only_acquire_returns_none_compensates(self, sync_limiter):
        """Nested parent failure: parent-only acquire returns None compensates child."""
        sync_limiter.create_entity("parent-1")
        sync_limiter.create_entity("child-1", parent_id="parent-1", cascade=True)
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1000)])
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}):
            pass
        now_ms = int(__import__("time").time() * 1000)
        child_bucket = BucketState(
            entity_id="child-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=900000,
            last_refill_ms=now_ms,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        parent_old = BucketState(
            entity_id="parent-1",
            resource="gpt-4",
            limit_name="rpm",
            tokens_milli=500000,
            last_refill_ms=now_ms - 30000,
            capacity_milli=1000000,
            refill_amount_milli=1000000,
            refill_period_ms=60000,
        )
        original_single = sync_limiter._repository._speculative_consume_single
        original_parent_acquire = sync_limiter._try_parent_only_acquire

        def mock_single(entity_id, resource, consume, ttl_seconds=None, shard_id=0):
            if entity_id == "child-1":
                return SpeculativeResult(
                    success=True, buckets=[child_bucket], cascade=True, parent_id="parent-1"
                )
            if entity_id == "parent-1":
                return SpeculativeResult(success=False, old_buckets=[parent_old])
            return original_single(entity_id, resource, consume, ttl_seconds)

        def mock_parent_acquire(*args, **kwargs):
            return None

        sync_limiter._repository._speculative_consume_single = mock_single
        sync_limiter._try_parent_only_acquire = mock_parent_acquire
        try:
            with sync_limiter.acquire("child-1", "gpt-4", {"rpm": 1}) as lease:
                assert len(lease.entries) > 0
        finally:
            sync_limiter._repository._speculative_consume_single = original_single
            sync_limiter._try_parent_only_acquire = original_parent_acquire


class TestShardRetry:
    """Tests for shard retry and doubling in acquire flow."""

    def test_acquire_retries_on_another_shard(self, sync_limiter):
        """When one shard's app limit is exhausted, retry on another shard."""
        repo = sync_limiter._repository
        ns = repo._namespace_id
        sync_limiter.create_entity("user-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        now_ms = int(time.time() * 1000)
        for shard_id in range(2):
            states = [
                BucketState.from_limit("user-1", "gpt-4", Limit.per_minute("rpm", 100), now_ms)
            ]
            put_item = repo.build_composite_create(
                "user-1", "gpt-4", states, now_ms, shard_id=shard_id, shard_count=2
            )
            repo.transact_write([put_item])
        repo._entity_cache[ns, "user-1"] = (False, None, {"gpt-4": 2})
        for _ in range(100):
            repo._speculative_consume_single("user-1", "gpt-4", {"rpm": 1}, shard_id=0)
        sync_limiter._speculative_writes = True
        with patch("zae_limiter.sync_repository.random.randrange", return_value=0):
            with sync_limiter.acquire("user-1", "gpt-4", {"rpm": 1}) as lease:
                rpm_entry = next(e for e in lease.entries if e.limit.name == "rpm")
                assert rpm_entry is not None

    def test_acquire_doubles_shards_on_wcu_exhaustion(self, sync_limiter):
        """When wcu is exhausted, shard_count doubles and acquire falls to slow path."""
        from zae_limiter import schema

        repo = sync_limiter._repository
        ns = repo._namespace_id
        sync_limiter.create_entity("user-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100000)])
        now_ms = int(time.time() * 1000)
        states = [
            BucketState.from_limit("user-1", "gpt-4", Limit.per_minute("rpm", 100000), now_ms)
        ]
        put_item = repo.build_composite_create(
            "user-1", "gpt-4", states, now_ms, shard_id=0, shard_count=1
        )
        repo.transact_write([put_item])
        repo._entity_cache[ns, "user-1"] = (False, None, {"gpt-4": 1})
        for _ in range(schema.WCU_LIMIT_CAPACITY):
            repo._speculative_consume_single("user-1", "gpt-4", {"rpm": 1}, shard_id=0)
        sync_limiter._speculative_writes = True
        with sync_limiter.acquire("user-1", "gpt-4", {"rpm": 1}) as lease:
            rpm_entry = next(e for e in lease.entries if e.limit.name == "rpm")
            assert rpm_entry is not None
        cache_entry = repo._entity_cache[ns, "user-1"]
        assert cache_entry[2]["gpt-4"] == 2

    def test_retry_on_other_shard_returns_none_when_all_exhausted(self, sync_limiter):
        """_retry_on_other_shard returns None when all shards are exhausted."""
        repo = sync_limiter._repository
        ns = repo._namespace_id
        sync_limiter.create_entity("user-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1)])
        now_ms = int(time.time() * 1000)
        for shard_id in range(2):
            states = [BucketState.from_limit("user-1", "gpt-4", Limit.per_minute("rpm", 1), now_ms)]
            put_item = repo.build_composite_create(
                "user-1", "gpt-4", states, now_ms, shard_id=shard_id, shard_count=2
            )
            repo.transact_write([put_item])
        repo._entity_cache[ns, "user-1"] = (False, None, {"gpt-4": 2})
        for shard_id in range(2):
            repo._speculative_consume_single("user-1", "gpt-4", {"rpm": 1}, shard_id=shard_id)
        sync_limiter._speculative_writes = True
        from zae_limiter.exceptions import RateLimitExceeded

        with pytest.raises(RateLimitExceeded):
            with sync_limiter.acquire("user-1", "gpt-4", {"rpm": 1}):
                pass


class TestWcuHiddenFromUser:
    """Tests that wcu infrastructure limit is hidden from user-facing output."""

    def test_rate_limit_exceeded_hides_wcu(self, sync_limiter):
        """wcu never appears in RateLimitExceeded statuses."""
        from zae_limiter.exceptions import RateLimitExceeded

        repo = sync_limiter._repository
        ns = repo._namespace_id
        sync_limiter.create_entity("user-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 1)])
        now_ms = int(time.time() * 1000)
        states = [BucketState.from_limit("user-1", "gpt-4", Limit.per_minute("rpm", 1), now_ms)]
        put_item = repo.build_composite_create(
            "user-1", "gpt-4", states, now_ms, shard_id=0, shard_count=1
        )
        repo.transact_write([put_item])
        repo._entity_cache[ns, "user-1"] = (False, None, {"gpt-4": 1})
        repo._speculative_consume_single("user-1", "gpt-4", {"rpm": 1}, shard_id=0)
        sync_limiter._speculative_writes = True
        with pytest.raises(RateLimitExceeded) as exc_info:
            with sync_limiter.acquire("user-1", "gpt-4", {"rpm": 1}):
                pass
        all_limit_names = [s.limit_name for s in exc_info.value.statuses]
        assert "wcu" not in all_limit_names

    def test_get_buckets_hides_wcu(self, sync_limiter):
        """get_buckets omits wcu from returned bucket states."""
        repo = sync_limiter._repository
        sync_limiter.create_entity("user-1")
        sync_limiter.set_system_defaults([Limit.per_minute("rpm", 100)])
        now_ms = int(time.time() * 1000)
        states = [BucketState.from_limit("user-1", "gpt-4", Limit.per_minute("rpm", 100), now_ms)]
        put_item = repo.build_composite_create(
            "user-1", "gpt-4", states, now_ms, shard_id=0, shard_count=1
        )
        repo.transact_write([put_item])
        buckets = repo.get_buckets("user-1", resource="gpt-4")
        limit_names = {b.limit_name for b in buckets}
        assert "wcu" not in limit_names
        assert "rpm" in limit_names
