"""
Integration tests for idempotency mechanism.

These tests verify that:
1. Normal stamps work with idempotency keys
2. Retries with same idempotency key return cached response without double-advance
3. Expected sequence provides useful diagnostics
"""

import asyncio
import pytest
import httpx
from unittest.mock import AsyncMock, patch, MagicMock

from lockstock_core.client import LockStockClient
from lockstock_core.types import VerifyStatus


class TestIdempotency:
    """Test idempotency mechanism."""

    @pytest.mark.asyncio
    async def test_normal_stamp_with_idempotency_key(self):
        """Test that normal stamps work with idempotency keys."""
        # Mock the httpx client
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.content = b'{"authorized": true, "state_hash": "abc123", "sequence": 5, "timestamp": 1234567890}'
        mock_response.json.return_value = {
            "authorized": True,
            "state_hash": "abc123",
            "sequence": 5,
            "timestamp": 1234567890,
        }

        # Also mock the sync call (get_chain_state)
        mock_sync_response = MagicMock()
        mock_sync_response.status_code = 200
        mock_sync_response.json.return_value = {
            "last_sequence": 4,
            "last_hash": "parent_hash",
        }

        async def mock_request(method, url, **kwargs):
            if "/state" in url:
                return mock_sync_response
            return mock_response

        with patch.object(httpx.AsyncClient, 'post', new_callable=AsyncMock) as mock_post, \
             patch.object(httpx.AsyncClient, 'get', new_callable=AsyncMock) as mock_get:
            mock_post.return_value = mock_response
            mock_get.return_value = mock_sync_response

            client = LockStockClient(
                agent_id="test_agent",
                api_key="test_key",
                endpoint="http://localhost:8080"
            )

            result = await client.stamp("chatcompletion")

            assert result.authorized
            assert result.state_hash == "abc123"
            assert result.sequence == 5

            # Verify idempotency_key was included in the request
            call_args = mock_post.call_args
            assert "idempotency_key" in call_args.kwargs["json"]

            await client.close()

    @pytest.mark.asyncio
    async def test_retry_returns_cached_response(self):
        """Test that retry with same idempotency key returns cached response."""
        # First call: timeout (simulates network drop after server processes)
        # Retry call: returns cached response

        call_count = 0
        mock_responses = []

        # First response: timeout
        # Second response: success (cached)
        mock_success = MagicMock()
        mock_success.status_code = 200
        mock_success.content = b'{"authorized": true, "state_hash": "abc123", "sequence": 5}'
        mock_success.json.return_value = {
            "authorized": True,
            "state_hash": "abc123",
            "sequence": 5,
        }

        # Sync response
        mock_sync_response = MagicMock()
        mock_sync_response.status_code = 200
        mock_sync_response.json.return_value = {"last_sequence": 4}

        async def mock_post(url, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                # First call: timeout
                raise httpx.TimeoutException("Connection timed out")
            else:
                # Retry: return cached response
                return mock_success

        async def mock_get(url, **kwargs):
            return mock_sync_response

        with patch.object(httpx.AsyncClient, 'post', side_effect=mock_post), \
             patch.object(httpx.AsyncClient, 'get', side_effect=mock_get):

            client = LockStockClient(
                agent_id="test_agent",
                api_key="test_key",
                endpoint="http://localhost:8080"
            )
            # Override retry delays to speed up test
            client.RETRY_DELAYS = [0.01, 0.01, 0.01]

            result = await client.stamp("chatcompletion")

            # Should succeed on retry
            assert result.authorized
            assert result.sequence == 5

            # Should have retried (2 calls total)
            assert call_count == 2

            await client.close()

    @pytest.mark.asyncio
    async def test_202_processing_retry(self):
        """Test that 202 (processing) triggers retry."""
        call_count = 0

        # First response: 202 (processing)
        mock_202 = MagicMock()
        mock_202.status_code = 202
        mock_202.content = b'{"status": "processing", "retry_after_ms": 100}'
        mock_202.json.return_value = {"status": "processing", "retry_after_ms": 100}

        # Second response: 200 (success)
        mock_200 = MagicMock()
        mock_200.status_code = 200
        mock_200.content = b'{"authorized": true, "sequence": 5}'
        mock_200.json.return_value = {"authorized": True, "sequence": 5}

        # Sync response
        mock_sync = MagicMock()
        mock_sync.status_code = 200
        mock_sync.json.return_value = {"last_sequence": 4}

        async def mock_post(url, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return mock_202
            return mock_200

        async def mock_get(url, **kwargs):
            return mock_sync

        with patch.object(httpx.AsyncClient, 'post', side_effect=mock_post), \
             patch.object(httpx.AsyncClient, 'get', side_effect=mock_get):

            client = LockStockClient(
                agent_id="test_agent",
                api_key="test_key",
                endpoint="http://localhost:8080"
            )

            result = await client.stamp("chatcompletion")

            assert result.authorized
            assert call_count == 2  # 202 then 200

            await client.close()

    @pytest.mark.asyncio
    async def test_409_concurrent_conflict_retry(self):
        """Test that 409 with concurrent conflict triggers retry with new key."""
        call_count = 0
        idempotency_keys = []

        # First response: 409 (concurrent conflict)
        mock_409 = MagicMock()
        mock_409.status_code = 409
        mock_409.content = b'{"reason": "Concurrent chain advance conflict"}'
        mock_409.json.return_value = {
            "reason": "Concurrent chain advance conflict",
            "current_sequence": 5,
        }

        # Second response: 200 (success)
        mock_200 = MagicMock()
        mock_200.status_code = 200
        mock_200.content = b'{"authorized": true, "sequence": 6}'
        mock_200.json.return_value = {"authorized": True, "sequence": 6}

        # Sync response
        mock_sync = MagicMock()
        mock_sync.status_code = 200
        mock_sync.json.return_value = {"last_sequence": 4}

        async def mock_post(url, **kwargs):
            nonlocal call_count
            call_count += 1
            idempotency_keys.append(kwargs["json"].get("idempotency_key"))
            if call_count == 1:
                return mock_409
            return mock_200

        async def mock_get(url, **kwargs):
            return mock_sync

        with patch.object(httpx.AsyncClient, 'post', side_effect=mock_post), \
             patch.object(httpx.AsyncClient, 'get', side_effect=mock_get):

            client = LockStockClient(
                agent_id="test_agent",
                api_key="test_key",
                endpoint="http://localhost:8080"
            )

            result = await client.stamp("chatcompletion")

            assert result.authorized
            assert call_count == 2

            # Verify different idempotency keys were used (concurrent = new request)
            assert idempotency_keys[0] != idempotency_keys[1]

            await client.close()

    @pytest.mark.asyncio
    async def test_409_sequence_mismatch_no_retry(self):
        """Test that 409 with sequence mismatch returns error (no retry)."""
        # 409 with sequence mismatch (not concurrent conflict)
        mock_409 = MagicMock()
        mock_409.status_code = 409
        mock_409.content = b'{"reason": "Client sequence stale"}'
        mock_409.json.return_value = {
            "reason": "Client sequence stale",
            "current_sequence": 10,
        }

        # Sync response
        mock_sync = MagicMock()
        mock_sync.status_code = 200
        mock_sync.json.return_value = {"last_sequence": 4}

        async def mock_post(url, **kwargs):
            return mock_409

        async def mock_get(url, **kwargs):
            return mock_sync

        with patch.object(httpx.AsyncClient, 'post', side_effect=mock_post), \
             patch.object(httpx.AsyncClient, 'get', side_effect=mock_get):

            client = LockStockClient(
                agent_id="test_agent",
                api_key="test_key",
                endpoint="http://localhost:8080"
            )

            result = await client.stamp("chatcompletion")

            # Should return error, not retry
            assert not result.authorized
            assert "stale" in result.reason.lower()
            assert result.sequence == 10

            await client.close()

    @pytest.mark.asyncio
    async def test_expected_sequence_sent(self):
        """Test that expected_sequence is sent after sync."""
        # Sync response
        mock_sync = MagicMock()
        mock_sync.status_code = 200
        mock_sync.json.return_value = {"last_sequence": 42}

        # Stamp response
        mock_stamp = MagicMock()
        mock_stamp.status_code = 200
        mock_stamp.content = b'{"authorized": true, "sequence": 43}'
        mock_stamp.json.return_value = {"authorized": True, "sequence": 43}

        sent_payload = None

        async def mock_post(url, **kwargs):
            nonlocal sent_payload
            sent_payload = kwargs["json"]
            return mock_stamp

        async def mock_get(url, **kwargs):
            return mock_sync

        with patch.object(httpx.AsyncClient, 'post', side_effect=mock_post), \
             patch.object(httpx.AsyncClient, 'get', side_effect=mock_get):

            client = LockStockClient(
                agent_id="test_agent",
                api_key="test_key",
                endpoint="http://localhost:8080"
            )

            result = await client.stamp("chatcompletion")

            assert result.authorized
            # Should have sent expected_sequence = 43 (42 + 1)
            assert sent_payload["expected_sequence"] == 43

            await client.close()

    @pytest.mark.asyncio
    async def test_lazy_sync_on_first_stamp(self):
        """Test that sync is called automatically on first stamp."""
        sync_called = False

        # Sync response
        mock_sync = MagicMock()
        mock_sync.status_code = 200
        mock_sync.json.return_value = {"last_sequence": 10}

        # Stamp response
        mock_stamp = MagicMock()
        mock_stamp.status_code = 200
        mock_stamp.content = b'{"authorized": true, "sequence": 11}'
        mock_stamp.json.return_value = {"authorized": True, "sequence": 11}

        async def mock_post(url, **kwargs):
            return mock_stamp

        async def mock_get(url, **kwargs):
            nonlocal sync_called
            sync_called = True
            return mock_sync

        with patch.object(httpx.AsyncClient, 'post', side_effect=mock_post), \
             patch.object(httpx.AsyncClient, 'get', side_effect=mock_get):

            client = LockStockClient(
                agent_id="test_agent",
                api_key="test_key",
                endpoint="http://localhost:8080"
            )

            # Verify sync hasn't been called yet
            assert not sync_called
            assert client._last_known_sequence is None

            # First stamp should trigger sync
            result = await client.stamp("chatcompletion")

            assert sync_called
            assert result.authorized

            await client.close()


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
