from __future__ import annotations

import asyncio
import httpx
import pytest

from contractlane_operator_sdk import APIError, AsyncOperatorClient, ClientOptions, KNOWN_ERROR_CODES, OperatorClient
from contractlane_operator_sdk.models import (
    AgentEnrollChallengeRequest,
    CreateOrgRequest,
    InviteAcceptRequest,
    MagicLinkStartRequest,
    RequestOptions,
    SignupStartRequest,
)


def _response(status: int, body: dict) -> httpx.Response:
    return httpx.Response(status_code=status, json=body)


def _mock_transport(responses: list[httpx.Response | Exception], calls: list[httpx.Request]) -> httpx.MockTransport:
    idx = {"i": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        calls.append(request)
        item = responses[min(idx["i"], len(responses) - 1)]
        idx["i"] += 1
        if isinstance(item, Exception):
            raise item
        return item

    return httpx.MockTransport(handler)


def test_signup_injects_idempotency_and_challenge_headers() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(201, {"request_id": "req_1", "signup_session": {}})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            challenge_headers=lambda _: {"X-Signup-Challenge": "abc"},
            idempotency_key_generator=lambda: "idem_test",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.public.signup.start(SignupStartRequest(email="a@example.com", org_name="Acme"))

    assert len(calls) == 1
    assert calls[0].headers["idempotency-key"] == "idem_test"
    assert calls[0].headers["x-signup-challenge"] == "abc"


def test_request_headers_override_challenge_provider_values() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(201, {"request_id": "req_1", "signup_session": {}})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            challenge_headers=lambda _: {
                "X-Signup-Challenge": "provider-signup",
                "X-Operator-Challenge": "provider-proof",
                "X-Operator-Challenge-Token": "provider-token",
            },
            idempotency_key_generator=lambda: "idem_test",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.public.signup.start(
        SignupStartRequest(email="a@example.com", org_name="Acme"),
        options=RequestOptions(
            headers={
                "X-Signup-Challenge": "request-signup",
                "X-Operator-Challenge": "request-proof",
                "X-Operator-Challenge-Token": "request-token",
            }
        ),
    )

    req = calls[-1]
    assert req.headers["x-signup-challenge"] == "request-signup"
    assert req.headers["x-operator-challenge"] == "request-proof"
    assert req.headers["x-operator-challenge-token"] == "request-token"


def test_session_auth_injection() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_2", "session": {}})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token=lambda: "session_tok",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.public.auth.session()

    assert calls[0].headers["authorization"] == "Bearer session_tok"


def test_gateway_operator_auth_and_no_idempotency() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_3"})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            operator_token="operator_tok",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.gateway.cel.create_contract({"contract": "x"})

    assert calls[0].headers["authorization"] == "Bearer operator_tok"
    assert "idempotency-key" not in calls[0].headers


def test_create_envelope_strips_unsafe_fields_and_keeps_canonical_subset() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_env"})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            operator_token="operator_tok",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.gateway.cel.create_envelope(
        {
            "template_id": "tpl_1",
            "variables": {"amount": 10},
            "counterparty": {"email": "x@example.com"},
            "principal_id": "prn_should_not_forward",
        }
    )

    body = calls[0].read().decode("utf-8")
    assert '"template_id":"tpl_1"' in body
    assert '"variables":{"amount":10}' in body
    assert '"counterparty":{"email":"x@example.com"}' in body
    assert "principal_id" not in body


def test_set_counterparty_keeps_canonical_fields_only() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_cp"})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            operator_token="operator_tok",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.gateway.cel.set_counterparty("ctr_1", {"email": "info+1@walletsocket.com", "name": "Counterparty", "participants": []})

    body = calls[0].read().decode("utf-8")
    assert '"counterparty"' in body
    assert '"email":"info+1@walletsocket.com"' in body
    assert '"name":"Counterparty"' in body
    assert "participants" not in body


def test_set_counterparties_deprecated_wrapper_warns_and_maps_to_singular() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_cp2"})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            operator_token="operator_tok",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    with pytest.warns(DeprecationWarning):
        client.gateway.cel.set_counterparties(
            "ctr_1",
            {"counterparties": [{"email": "info+1@walletsocket.com", "role": "SIGNER"}]},
        )

    body = calls[0].read().decode("utf-8")
    assert '"counterparty"' in body
    assert "counterparties" not in body


def test_explicit_idempotency_override() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(201, {"request_id": "req_1", "org": {}})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            default_headers={"Idempotency-Key": "idem_default"},
            idempotency_key_generator=lambda: "idem_generated",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.operator.admin.create_org(
        CreateOrgRequest(name="Acme", admin_email="owner@example.com"),
        RequestOptions(idempotency_key="idem_override"),
    )

    assert calls[0].headers["idempotency-key"] == "idem_override"


def test_path_parameters_are_fully_escaped() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_1", "signup_session": {}})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.public.signup.get("a/b")
    assert str(calls[0].url).endswith("/public/v1/signup/a%2Fb")


def test_error_mapping() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport(
        [
            _response(
                400,
                {
                    "request_id": "req_err",
                    "code": "BAD_REQUEST",
                    "message": "invalid",
                    "meta": {"reason": "x"},
                },
            )
        ],
        calls,
    )
    client = OperatorClient(ClientOptions(base_url="https://example.test", httpx_client=httpx.Client(transport=transport)))

    with pytest.raises(APIError) as exc:
        client.public.auth.magic_link_start(MagicLinkStartRequest(email="x@example.com"))

    assert exc.value.status == 400
    assert exc.value.code == "BAD_REQUEST"
    assert exc.value.request_id == "req_err"


def test_preserves_template_not_enabled_business_code() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport(
        [
            _response(
                404,
                {
                    "request_id": "req_tpl",
                    "code": "TEMPLATE_NOT_ENABLED_FOR_PROJECT",
                    "message": "template is not available",
                },
            )
        ],
        calls,
    )
    client = OperatorClient(
        ClientOptions(base_url="https://example.test", operator_token="operator", httpx_client=httpx.Client(transport=transport))
    )

    with pytest.raises(APIError) as exc:
        client.gateway.cel.create_contract({})

    assert exc.value.code == "TEMPLATE_NOT_ENABLED_FOR_PROJECT"
    assert exc.value.request_id == "req_tpl"
    assert exc.value.code in KNOWN_ERROR_CODES


def test_retry_safe_get() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport(
        [
            _response(500, {"request_id": "req_fail", "code": "INTERNAL_ERROR", "message": "nope"}),
            _response(200, {"request_id": "req_ok", "session": {}}),
        ],
        calls,
    )
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    result = client.public.auth.session()
    assert result.meta.request_id == "req_ok"
    assert len(calls) == 2


def test_no_retry_gateway_mutation() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport(
        [
            _response(502, {"request_id": "req_up", "code": "UPSTREAM_ERROR", "message": "bad upstream"}),
            _response(200, {"request_id": "req_should_not_happen"}),
        ],
        calls,
    )
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            operator_token="operator",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    with pytest.raises(APIError):
        client.gateway.cel.create_contract({})

    assert len(calls) == 1


def test_sync_history_pagination() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport(
        [
            _response(200, {"request_id": "req_1", "history": [{"envelope_id": "env_1"}], "next_page_token": "nxt"}),
            _response(200, {"request_id": "req_2", "history": [{"envelope_id": "env_2"}]}),
        ],
        calls,
    )
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    items = list(client.operator.history.iter_all())
    assert len(items) == 2
    assert items[0]["envelope_id"] == "env_1"
    assert items[1]["envelope_id"] == "env_2"


def test_pending_history_is_normalized_not_error() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_hist"})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    result = client.operator.history.get("env_pending")
    history = result.data["history"]
    assert history["envelope_id"] == "env_pending"
    assert history["status"] == "PENDING"
    assert history["events"] == []


def test_envelopes_list_serialization() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": "req_env", "items": [], "next_page_token": "nxt"})], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    result = client.operator.envelopes.list(
        {
            "include_terminal": True,
            "sort_by": "updated_at",
            "sort_order": "desc",
            "page_size": 10,
            "page_token": "tok_1",
        }
    )
    assert result.meta.request_id == "req_env"
    assert "/operator/v1/envelopes?" in str(calls[0].url)
    assert "include_terminal=true" in str(calls[0].url)
    assert "sort_by=updated_at" in str(calls[0].url)


def test_envelopes_get_paths() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport(
        [
            _response(200, {"request_id": "req_ctr", "envelope": {"contract_id": "ctr_1"}}),
            _response(200, {"request_id": "req_env", "envelope": {"envelope_id": "env_1"}}),
        ],
        calls,
    )
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.operator.envelopes.get("ctr_1")
    client.operator.envelopes.get_by_envelope_id("env_1")

    assert "/operator/v1/envelopes/ctr_1" in str(calls[0].url)
    assert "/operator/v1/envelopes/by-envelope/env_1" in str(calls[1].url)


def test_envelopes_error_passthrough_codes() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport(
        [
            _response(404, {"request_id": "req_1", "code": "ENVELOPE_NOT_FOUND", "message": "envelope not found"}),
            _response(400, {"request_id": "req_2", "code": "BAD_PAGE_TOKEN", "message": "invalid page token"}),
            _response(400, {"request_id": "req_3", "code": "BAD_SORT_FIELD", "message": "unsupported sort field"}),
        ],
        calls,
    )
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            httpx_client=httpx.Client(transport=transport),
        )
    )
    with pytest.raises(APIError) as e1:
        client.operator.envelopes.get("ctr_missing")
    assert e1.value.code == "ENVELOPE_NOT_FOUND"
    with pytest.raises(APIError) as e2:
        client.operator.envelopes.list({"page_token": "bad"})
    assert e2.value.code == "BAD_PAGE_TOKEN"
    with pytest.raises(APIError) as e3:
        client.operator.envelopes.list({"sort_by": "state"})
    assert e3.value.code == "BAD_SORT_FIELD"


def test_async_client_and_pagination() -> None:
    async def run() -> tuple[list[str], str | None]:
        calls: list[httpx.Request] = []
        transport = _mock_transport(
            [
                _response(200, {"request_id": "req_1", "history": [{"envelope_id": "env_1"}], "next_page_token": "nxt"}),
                _response(200, {"request_id": "req_2", "history": [{"envelope_id": "env_2"}]}),
            ],
            calls,
        )
        client = AsyncOperatorClient(
            ClientOptions(
                base_url="https://example.test",
                session_token="session",
                httpx_async_client=httpx.AsyncClient(transport=transport),
            )
        )

        item_ids: list[str] = []
        async for item in client.operator.history.aiter_all():
            item_ids.append(item["envelope_id"])

        result = await client.public.agent_enrollment.challenge(
            AgentEnrollChallengeRequest(public_key_jwk={"kty": "OKP", "crv": "Ed25519", "x": "abc"})
        )
        await client.aclose()
        return item_ids, result.meta.request_id

    item_ids, request_id = asyncio.run(run())
    assert item_ids == ["env_1", "env_2"]
    assert request_id == "req_2"


def test_group_calls_coverage_smoke() -> None:
    calls: list[httpx.Request] = []
    transport = _mock_transport([_response(200, {"request_id": f"req_{i}"}) for i in range(13)], calls)
    client = OperatorClient(
        ClientOptions(
            base_url="https://example.test",
            session_token="session",
            operator_token="operator",
            httpx_client=httpx.Client(transport=transport),
        )
    )

    client.public.invites.accept(InviteAcceptRequest(invite_token="tok", email="x@example.com"))
    client.public.agent_enrollment.get("age_1")
    client.operator.admin.list_actors("prj_1")
    client.operator.security.list_abuse_events(10)
    client.operator.templates.list()
    client.operator.history.get("env_1")
    client.public.shared_history.get("share_tok")
    client.public.signing.resolve("sgn_tok")
    client.operator.admin.list_actors_compat("prj_1")
    client.operator.actor_keys.list("act_1")
    client.operator.envelopes.list()
    client.gateway.cel.proof_bundle("ctr_1")
    client.public.auth.sessions()

    assert len(calls) == 13
