"""Tests for MeshLedgerClient — async and sync, with mocked HTTP via respx."""

import pytest
import httpx
import respx

from meshledger import (
    MeshLedgerClient,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    MeshLedgerError,
)

BASE = "https://meshledger.io/api/v1"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def ok(data):
    """Wrap data in the API success envelope."""
    return httpx.Response(200, json={"success": True, "data": data})


def created(data):
    return httpx.Response(201, json={"success": True, "data": data})


def err(status, error="fail", code="ERROR"):
    return httpx.Response(status, json={"success": False, "error": error, "code": code})


# ---------------------------------------------------------------------------
# Registration (no auth)
# ---------------------------------------------------------------------------


class TestRegistration:
    @pytest.mark.asyncio
    @respx.mock
    async def test_register_async(self):
        respx.post(f"{BASE}/agents/register").mock(
            return_value=httpx.Response(
                201,
                json={
                    "success": True,
                    "data": {
                        "agent_id": "a1",
                        "api_key": "ml_sk_test123",
                        "name": "my-agent",
                        "created_at": "2025-01-01T00:00:00Z",
                    },
                    "warning": "Save your API key now.",
                },
            )
        )

        result = await MeshLedgerClient.register(
            name="my-agent",
            description="Test agent",
            wallet_address="0xabc",
            chains=["base"],
            capabilities=["testing"],
        )
        assert result.agent_id == "a1"
        assert result.api_key == "ml_sk_test123"

    @respx.mock
    def test_register_sync(self):
        respx.post(f"{BASE}/agents/register").mock(
            return_value=httpx.Response(
                201,
                json={
                    "success": True,
                    "data": {
                        "agent_id": "a1",
                        "api_key": "ml_sk_test456",
                        "name": "my-agent",
                    },
                },
            )
        )

        result = MeshLedgerClient.register_sync(
            name="my-agent",
            description="Test",
            wallet_address="0xabc",
            chains=["base"],
        )
        assert result.api_key == "ml_sk_test456"


# ---------------------------------------------------------------------------
# Skills (public browsing)
# ---------------------------------------------------------------------------


class TestSkills:
    @pytest.mark.asyncio
    @respx.mock
    async def test_list_skills_no_auth(self):
        respx.get(f"{BASE}/skills").mock(
            return_value=ok(
                {
                    "skills": [
                        {
                            "skill_id": "s1",
                            "agent_id": "a1",
                            "name": "Code Review",
                            "description": "Reviews code",
                            "capabilities": ["code-review"],
                            "price": 5.0,
                            "price_token": "USDC",
                            "price_chain": "base",
                        }
                    ],
                    "cursor": None,
                    "has_more": False,
                }
            )
        )

        client = MeshLedgerClient()  # no api_key
        result = await client.skills.list()
        assert len(result.items) == 1
        assert result.has_more is False
        await client.aclose()

    @respx.mock
    def test_list_skills_sync(self):
        respx.get(f"{BASE}/skills").mock(
            return_value=ok({"skills": [], "cursor": None, "has_more": False})
        )

        client = MeshLedgerClient()
        result = client.skills.list_sync()
        assert result.items == []
        client.close()

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_skill(self):
        respx.get(f"{BASE}/skills/s1").mock(
            return_value=ok(
                {
                    "skill_id": "s1",
                    "agent_id": "a1",
                    "name": "Test",
                    "description": "Desc",
                    "capabilities": ["test"],
                    "price": 3.0,
                    "price_token": "USDC",
                    "price_chain": "base",
                }
            )
        )

        client = MeshLedgerClient()
        skill = await client.skills.get("s1")
        assert skill.skill_id == "s1"
        assert skill.price == 3.0
        await client.aclose()

    @pytest.mark.asyncio
    @respx.mock
    async def test_create_skill_requires_auth(self):
        client = MeshLedgerClient()  # no api key
        with pytest.raises(AuthenticationError):
            await client.skills.create(
                name="Test",
                description="Desc",
                capabilities=["test"],
                price=5.0,
                price_token="USDC",
                price_chain="base",
            )
        await client.aclose()


# ---------------------------------------------------------------------------
# Jobs (authenticated flow)
# ---------------------------------------------------------------------------


class TestJobs:
    @pytest.mark.asyncio
    @respx.mock
    async def test_create_job(self):
        respx.post(f"{BASE}/jobs").mock(
            return_value=httpx.Response(
                201,
                json={
                    "success": True,
                    "data": {
                        "job_id": "j1",
                        "escrow_tx_hash": "0xabc",
                        "total_charged": 10.5,
                        "listed_price": 10.0,
                        "service_fee": 0.5,
                        "status": "funded",
                        "created_at": "2025-01-01T00:00:00Z",
                    },
                },
            )
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            job = await client.jobs.create(
                title="Test Job",
                description="Test",
                chain="base",
                token="USDC",
                price=10.0,
            )
            assert job.job_id == "j1"
            assert job.total_charged == 10.5
            assert job.status.value == "funded"

    @pytest.mark.asyncio
    @respx.mock
    async def test_accept_deliver_release_flow(self):
        # Accept
        respx.post(f"{BASE}/jobs/j1/accept").mock(
            return_value=ok(
                {
                    "job_id": "j1",
                    "title": "Test",
                    "status": "accepted",
                    "customer_id": "c1",
                    "deliverer_id": "d1",
                }
            )
        )
        # Deliver
        respx.post(f"{BASE}/jobs/j1/deliver").mock(
            return_value=ok(
                {
                    "job_id": "j1",
                    "status": "delivered",
                    "auto_release_at": "2025-01-02T00:00:00Z",
                    "message": "Customer has 24 hours.",
                }
            )
        )
        # Release
        respx.post(f"{BASE}/jobs/j1/release").mock(
            return_value=ok(
                {
                    "job_id": "j1",
                    "status": "released",
                    "release_tx_hash": "0xdef",
                    "amount_to_deliverer": 9.5,
                    "fee_to_treasury": 0.5,
                }
            )
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            accepted = await client.jobs.accept("j1")
            assert accepted.status.value == "accepted"

            delivered = await client.jobs.deliver(
                "j1", deliverable_text="Here is the work"
            )
            assert delivered.status == "delivered"

            released = await client.jobs.release("j1")
            assert released.amount_to_deliverer == 9.5
            assert released.release_tx_hash == "0xdef"

    @pytest.mark.asyncio
    @respx.mock
    async def test_cancel_job(self):
        respx.post(f"{BASE}/jobs/j1/cancel").mock(
            return_value=ok(
                {
                    "job_id": "j1",
                    "status": "cancelled",
                    "amount_refunded": 10.0,
                    "refund_tx_hash": "0xaaa",
                }
            )
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            result = await client.jobs.cancel("j1")
            assert result.status == "cancelled"
            assert result.amount_refunded == 10.0

    @pytest.mark.asyncio
    @respx.mock
    async def test_dispute_job(self):
        respx.post(f"{BASE}/jobs/j1/dispute").mock(
            return_value=ok(
                {
                    "dispute_id": "d1",
                    "job_id": "j1",
                    "status": "disputed",
                    "message": "AI Judge will evaluate within 30 minutes.",
                }
            )
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            result = await client.jobs.dispute("j1", reason="Bad work")
            assert result.dispute_id == "d1"

    @pytest.mark.asyncio
    @respx.mock
    async def test_rate_job(self):
        respx.post(f"{BASE}/jobs/j1/rate").mock(
            return_value=httpx.Response(
                201,
                json={
                    "success": True,
                    "data": {
                        "rating_id": "r1",
                        "job_id": "j1",
                        "rated_id": "a2",
                        "score": 5,
                        "comment": "Great work",
                        "created_at": "2025-01-01T00:00:00Z",
                    },
                },
            )
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            result = await client.jobs.rate("j1", score=5, comment="Great work")
            assert result.score == 5


# ---------------------------------------------------------------------------
# Agents
# ---------------------------------------------------------------------------


class TestAgents:
    @pytest.mark.asyncio
    @respx.mock
    async def test_me(self):
        respx.get(f"{BASE}/agents/me").mock(
            return_value=ok(
                {
                    "agent_id": "a1",
                    "name": "test-agent",
                    "description": "Test",
                    "chains": ["base"],
                    "capabilities": ["test"],
                }
            )
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            agent = await client.agents.me()
            assert agent.agent_id == "a1"

    @pytest.mark.asyncio
    @respx.mock
    async def test_search_agents(self):
        respx.get(f"{BASE}/agents/search").mock(
            return_value=ok(
                {"agents": [{"agent_id": "a1", "name": "test", "description": "d", "chains": ["base"], "capabilities": []}], "cursor": None, "has_more": False}
            )
        )

        client = MeshLedgerClient()
        result = await client.agents.search(capabilities=["code-review"])
        assert len(result.items) == 1
        await client.aclose()

    @pytest.mark.asyncio
    @respx.mock
    async def test_dashboard(self):
        respx.get(f"{BASE}/agents/me/dashboard").mock(
            return_value=ok({"agent": {"agent_id": "a1"}, "stats": {"completed_jobs": 5}})
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            dash = await client.agents.dashboard()
            assert dash.agent is not None


# ---------------------------------------------------------------------------
# Marketplace
# ---------------------------------------------------------------------------


class TestMarketplace:
    @pytest.mark.asyncio
    @respx.mock
    async def test_stats(self):
        respx.get(f"{BASE}/marketplace/stats").mock(
            return_value=ok(
                {
                    "total_agents": 100,
                    "total_skills": 500,
                    "total_jobs_completed": 1000,
                    "total_volume_usd": 50000.0,
                    "active_escrows": 25,
                    "average_completion_time_minutes": 45.0,
                }
            )
        )

        client = MeshLedgerClient()
        stats = await client.marketplace.stats()
        assert stats.total_agents == 100
        assert stats.active_escrows == 25
        await client.aclose()

    @respx.mock
    def test_stats_sync(self):
        respx.get(f"{BASE}/marketplace/stats").mock(
            return_value=ok(
                {
                    "total_agents": 50,
                    "total_skills": 200,
                    "total_jobs_completed": 500,
                    "total_volume_usd": 25000.0,
                    "active_escrows": 10,
                }
            )
        )

        client = MeshLedgerClient()
        stats = client.marketplace.stats_sync()
        assert stats.total_agents == 50
        client.close()


# ---------------------------------------------------------------------------
# Disputes
# ---------------------------------------------------------------------------


class TestDisputes:
    @pytest.mark.asyncio
    @respx.mock
    async def test_list_disputes(self):
        respx.get(f"{BASE}/disputes/public").mock(
            return_value=ok({"disputes": [], "cursor": None, "has_more": False})
        )

        client = MeshLedgerClient()
        result = await client.disputes.list()
        assert result.items == []
        await client.aclose()

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_dispute(self):
        respx.get(f"{BASE}/disputes/d1").mock(
            return_value=ok(
                {
                    "dispute_id": "d1",
                    "job_id": "j1",
                    "status": "disputed",
                    "reason": "Bad work",
                }
            )
        )

        client = MeshLedgerClient()
        dispute = await client.disputes.get("d1")
        assert dispute.dispute_id == "d1"
        await client.aclose()


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------


class TestErrors:
    @pytest.mark.asyncio
    @respx.mock
    async def test_401_raises_auth_error(self):
        respx.get(f"{BASE}/agents/me").mock(
            return_value=err(401, "Invalid API key", "AUTH_REQUIRED")
        )

        async with MeshLedgerClient(api_key="ml_sk_bad") as client:
            with pytest.raises(AuthenticationError) as exc_info:
                await client.agents.me()
            assert exc_info.value.status == 401

    @pytest.mark.asyncio
    @respx.mock
    async def test_404_raises_not_found(self):
        respx.get(f"{BASE}/jobs/nonexistent").mock(
            return_value=err(404, "Job not found", "NOT_FOUND")
        )

        client = MeshLedgerClient()
        with pytest.raises(NotFoundError):
            await client.jobs.get("nonexistent")
        await client.aclose()

    @pytest.mark.asyncio
    @respx.mock
    async def test_429_raises_rate_limit(self):
        respx.get(f"{BASE}/skills").mock(
            return_value=httpx.Response(
                429,
                json={"success": False, "error": "Rate limited", "code": "RATE_LIMITED"},
                headers={"Retry-After": "0"},
            )
        )

        client = MeshLedgerClient()
        with pytest.raises(RateLimitError):
            await client.skills.list()
        await client.aclose()

    @pytest.mark.asyncio
    @respx.mock
    async def test_400_raises_validation_error(self):
        respx.post(f"{BASE}/jobs").mock(
            return_value=err(400, "Price must be >= 1", "VALIDATION_ERROR")
        )

        async with MeshLedgerClient(api_key="ml_sk_test") as client:
            with pytest.raises(ValidationError) as exc_info:
                await client.jobs.create(
                    title="Bad",
                    description="Bad",
                    chain="base",
                    token="USDC",
                    price=0,
                )
            assert "Price" in exc_info.value.message

    def test_no_api_key_raises_auth_error(self):
        client = MeshLedgerClient()  # no key
        with pytest.raises(AuthenticationError):
            client.agents.me_sync()
        client.close()


# ---------------------------------------------------------------------------
# Context managers
# ---------------------------------------------------------------------------


class TestContextManagers:
    @pytest.mark.asyncio
    @respx.mock
    async def test_async_context_manager(self):
        respx.get(f"{BASE}/marketplace/stats").mock(
            return_value=ok(
                {
                    "total_agents": 1,
                    "total_skills": 1,
                    "total_jobs_completed": 1,
                    "total_volume_usd": 1.0,
                    "active_escrows": 0,
                }
            )
        )

        async with MeshLedgerClient() as client:
            stats = await client.marketplace.stats()
            assert stats.total_agents == 1
        # client should be closed after exiting
        assert client._async_client is None

    @respx.mock
    def test_sync_context_manager(self):
        respx.get(f"{BASE}/marketplace/stats").mock(
            return_value=ok(
                {
                    "total_agents": 2,
                    "total_skills": 2,
                    "total_jobs_completed": 2,
                    "total_volume_usd": 2.0,
                    "active_escrows": 0,
                }
            )
        )

        with MeshLedgerClient() as client:
            stats = client.marketplace.stats_sync()
            assert stats.total_agents == 2
        assert client._sync_client is None


# ---------------------------------------------------------------------------
# Sync wrappers
# ---------------------------------------------------------------------------


class TestSyncWrappers:
    @respx.mock
    def test_jobs_create_sync(self):
        respx.post(f"{BASE}/jobs").mock(
            return_value=httpx.Response(
                201,
                json={
                    "success": True,
                    "data": {
                        "job_id": "j1",
                        "escrow_tx_hash": "0x123",
                        "total_charged": 5.25,
                        "listed_price": 5.0,
                        "service_fee": 0.25,
                        "status": "funded",
                    },
                },
            )
        )

        with MeshLedgerClient(api_key="ml_sk_test") as client:
            job = client.jobs.create_sync(
                title="Sync Job",
                description="Test",
                chain="base",
                token="USDC",
                price=5.0,
            )
            assert job.job_id == "j1"

    @respx.mock
    def test_agents_me_sync(self):
        respx.get(f"{BASE}/agents/me").mock(
            return_value=ok(
                {
                    "agent_id": "a1",
                    "name": "test",
                    "description": "d",
                    "chains": ["base"],
                    "capabilities": [],
                }
            )
        )

        with MeshLedgerClient(api_key="ml_sk_test") as client:
            agent = client.agents.me_sync()
            assert agent.name == "test"

    @respx.mock
    def test_disputes_list_sync(self):
        respx.get(f"{BASE}/disputes/public").mock(
            return_value=ok({"disputes": [], "cursor": None, "has_more": False})
        )

        with MeshLedgerClient() as client:
            result = client.disputes.list_sync()
            assert result.items == []
