"""Contract tests for the complete channel exchange outcome."""

import pytest
from agenteval.channels.base import (
    ChannelHealth,
    ChannelTransportError,
    EvalChannel,
    ExchangeOutcome,
    ExchangeStatus,
    Reply,
    SendResult,
    normalize_reply_text,
)


class ContractChannel(EvalChannel):
    def __init__(self, *, send_result=None, reply=None, poll_error=None):
        self.send_result = send_result or SendResult(ok=True, question_msg_id="question-7")
        self.reply = reply
        self.poll_error = poll_error
        self.events: list[str] = []

    async def health_check(self):
        return ChannelHealth(ok=True)

    async def _send(self, content, **kwargs):
        self.events.append("send")
        return self.send_result

    async def _poll_reply(self, question_msg_id, timeout=30.0, poll_interval=1.0):
        self.events.append("poll")
        if self.poll_error:
            raise self.poll_error
        return self.reply


def test_success_normalizes_reply_and_preserves_exchange_metadata():
    diagnostic = {"provider": "tutu", "request_id": "req-7"}

    outcome = ExchangeOutcome.succeeded(
        correlation_id="question-7",
        reply={"msgBody": {"content": "你好"}},
        latency_ms=128,
        diagnostic=diagnostic,
    )

    assert outcome.status is ExchangeStatus.SUCCESS
    assert outcome.ok is True
    assert outcome.expected_failure is False
    assert outcome.correlation_id == "question-7"
    assert outcome.reply_text == "你好"
    assert outcome.latency_ms == 128
    assert outcome.diagnostic is diagnostic


@pytest.mark.parametrize(
    ("outcome", "status"),
    [
        (ExchangeOutcome.send_failed("connection refused"), ExchangeStatus.SEND_FAILED),
        (ExchangeOutcome.reply_timeout(correlation_id="question-7", latency_ms=30_000), ExchangeStatus.REPLY_TIMEOUT),
        (
            ExchangeOutcome.poll_failed("upstream returned 502", correlation_id="question-7"),
            ExchangeStatus.POLL_FAILED,
        ),
    ],
)
def test_expected_transport_failures_have_typed_status(outcome, status):
    assert outcome.status is status
    assert outcome.ok is False
    assert outcome.expected_failure is True


def test_normalize_reply_text_handles_provider_shapes():
    assert normalize_reply_text("plain text") == "plain text"
    assert normalize_reply_text({"content": "content field"}) == "content field"
    assert normalize_reply_text({"msgBody": {"text": "nested text"}}) == "nested text"
    assert normalize_reply_text(None) == ""


def test_success_requires_correlation_text_and_latency():
    with pytest.raises(ValueError, match="correlation_id"):
        ExchangeOutcome(status=ExchangeStatus.SUCCESS, reply_text="ok", latency_ms=1)

    with pytest.raises(ValueError, match="reply_text"):
        ExchangeOutcome(status=ExchangeStatus.SUCCESS, correlation_id="question-7", latency_ms=1)

    with pytest.raises(ValueError, match="latency_ms"):
        ExchangeOutcome(status=ExchangeStatus.SUCCESS, correlation_id="question-7", reply_text="ok", latency_ms=-1)


async def test_exchange_runs_send_hook_before_polling_and_returns_success():
    channel = ContractChannel(reply=Reply(question_msg_id="question-7", content={"content": "答复"}))

    async def on_sent(send_result):
        channel.events.append(f"hook:{send_result.question_msg_id}")

    outcome = await channel.exchange("问题", on_sent=on_sent)

    assert outcome.ok is True
    assert outcome.reply_text == "答复"
    assert outcome.correlation_id == "question-7"
    assert channel.events == ["send", "hook:question-7", "poll"]


async def test_exchange_send_failure_skips_hook_and_polling():
    channel = ContractChannel(send_result=SendResult(ok=False, error="offline"))

    async def on_sent(_send_result):
        raise AssertionError("send hook must not run after a failed send")

    outcome = await channel.exchange("问题", on_sent=on_sent)

    assert outcome.status is ExchangeStatus.SEND_FAILED
    assert outcome.reason == "offline"
    assert channel.events == ["send"]


async def test_exchange_hook_failure_skips_polling():
    channel = ContractChannel(reply=Reply(question_msg_id="question-7", content="答复"))

    async def on_sent(_send_result):
        channel.events.append("hook")
        raise RuntimeError("ledger unavailable")

    with pytest.raises(RuntimeError, match="ledger unavailable"):
        await channel.exchange("问题", on_sent=on_sent)

    assert channel.events == ["send", "hook"]


async def test_exchange_distinguishes_timeout_and_poll_failure():
    timeout_channel = ContractChannel()
    timeout = await timeout_channel.exchange("问题")
    assert timeout.status is ExchangeStatus.REPLY_TIMEOUT
    assert timeout.correlation_id == "question-7"

    failed_channel = ContractChannel(poll_error=ChannelTransportError("upstream unavailable"))
    failed = await failed_channel.exchange("问题")
    assert failed.status is ExchangeStatus.POLL_FAILED
    assert failed.reason == "upstream unavailable"


async def test_exchange_does_not_mask_programming_or_configuration_errors():
    channel = ContractChannel(poll_error=ValueError("invalid adapter configuration"))

    with pytest.raises(ValueError, match="invalid adapter configuration"):
        await channel.exchange("问题")
