import asyncio
import json

from fastapi.testclient import TestClient

from beaver.foundation.config.schema import ChannelConfig
from beaver.foundation.events import ChannelIdentity, InboundMessage, OutboundMessage
from beaver.foundation.events import MessageBus
from beaver.interfaces.channels.generic_webhook import GenericWebhookAdapter
from beaver.interfaces.channels.runtime import ChannelRuntime
from beaver.interfaces.channels.state import ChannelDedupeStore, ChannelEventLog
from beaver.interfaces.web.app import _self_restart_enabled, create_app
from beaver.services.agent_service import AgentService


def test_channel_identity_builds_stable_session_id() -> None:
    identity = ChannelIdentity(
        channel_id="webhook-dev",
        kind="webhook",
        account_id="local",
        peer_id="demo-user",
        thread_id="main",
        peer_type="dm",
        message_id="msg-1",
    )

    assert identity.session_id() == "webhook-dev:local:demo-user:main"
    assert identity.dedupe_key() == "webhook-dev:local:demo-user:main:msg-1"


def test_channel_identity_requires_routing_fields() -> None:
    identity = ChannelIdentity(channel_id="webhook-dev", kind="webhook", account_id="", peer_id="demo")

    assert identity.validation_error() == "account_id is required"


def test_messages_carry_channel_identity() -> None:
    identity = ChannelIdentity(
        channel_id="webhook-dev",
        kind="webhook",
        account_id="local",
        peer_id="demo-user",
        message_id="msg-1",
    )

    inbound = InboundMessage(channel="webhook-dev", content="hello", channel_identity=identity)
    outbound = OutboundMessage(
        channel="webhook-dev",
        content="ok",
        session_id=identity.session_id(),
        finish_reason="stop",
        channel_identity=identity,
    )

    assert inbound.channel_identity is identity
    assert outbound.channel_identity is identity


def test_dedupe_store_tracks_processing_and_done(tmp_path) -> None:
    store = ChannelDedupeStore(tmp_path / "dedupe.json", retention_hours=48)

    created = store.mark_processing(
        dedupe_key="webhook-dev:local:demo:msg-1",
        session_id="webhook-dev:local:demo",
        message_id="msg-1",
    )
    duplicate = store.mark_processing(
        dedupe_key="webhook-dev:local:demo:msg-1",
        session_id="webhook-dev:local:demo",
        message_id="msg-1",
    )

    assert created.created is True
    assert duplicate.created is False
    assert duplicate.record is not None
    assert duplicate.record["status"] == "processing"

    store.mark_done(
        dedupe_key="webhook-dev:local:demo:msg-1",
        run_id="run-1",
        reply="hello" * 10000,
        max_reply_chars=20,
    )

    done = store.get("webhook-dev:local:demo:msg-1")
    assert done is not None
    assert done["status"] == "done"
    assert done["reply"] == "hellohellohellohello"


def test_channel_event_log_writes_recent_events(tmp_path) -> None:
    log = ChannelEventLog(tmp_path / "events.jsonl")
    log.record(
        channel_id="webhook-dev",
        kind="inbound_accepted",
        session_id="webhook-dev:local:demo",
        message_id="msg-1",
        status="ok",
        text="hello world",
    )

    events = log.recent(channel_id="webhook-dev", limit=10)

    assert len(events) == 1
    assert events[0]["kind"] == "inbound_accepted"
    assert events[0]["text_preview"] == "hello world"
    assert "raw_channel_payload" not in json.dumps(events[0])


class FakeAgentService:
    is_running = True

    async def handle_inbound_message(self, inbound):
        return OutboundMessage(
            message_id=inbound.message_id,
            channel=inbound.channel,
            content=f"echo:{inbound.content}",
            session_id=inbound.session_id,
            finish_reason="stop",
            run_id="run-1",
            channel_identity=inbound.channel_identity,
        )


class SlowFakeAgentService(FakeAgentService):
    async def handle_inbound_message(self, inbound):
        await asyncio.sleep(0.05)
        return await super().handle_inbound_message(inbound)


def test_channel_runtime_accept_inbound_normalizes_session_and_dedupes(tmp_path) -> None:
    async def run() -> None:
        bus = MessageBus()
        runtime = ChannelRuntime(
            service=FakeAgentService(),
            bus=bus,
            workspace=tmp_path,
            channels={},
        )
        identity = ChannelIdentity(
            channel_id="webhook-dev",
            kind="webhook",
            account_id="local",
            peer_id="demo",
            message_id="msg-1",
        )
        result = await runtime.accept_inbound(
            InboundMessage(
                channel="webhook-dev",
                content="hello",
                session_id="wrong",
                channel_identity=identity,
            )
        )
        duplicate = await runtime.accept_inbound(
            InboundMessage(
                channel="webhook-dev",
                content="hello",
                channel_identity=identity,
            )
        )

        queued = await bus.consume_inbound()
        assert result.accepted is True
        assert queued.session_id == "webhook-dev:local:demo"
        assert duplicate.accepted is False
        assert duplicate.duplicate is True

    asyncio.run(run())


def test_generic_webhook_adapter_waits_for_outbound_reply(tmp_path) -> None:
    async def run() -> None:
        bus = MessageBus()
        runtime = ChannelRuntime(
            service=FakeAgentService(),
            bus=bus,
            workspace=tmp_path,
            channels={},
        )
        adapter = GenericWebhookAdapter(
            channel_id="webhook-dev",
            kind="webhook",
            mode="webhook",
            account_id="local",
            display_name="Webhook Dev",
            inbound_sink=runtime,
            response_timeout_seconds=1,
        )
        runtime.manager.register(adapter)
        await runtime.start()
        try:
            response = await adapter.handle_webhook_payload(
                {
                    "peer_id": "demo",
                    "message_id": "msg-1",
                    "text": "hello",
                    "peer_type": "dm",
                }
            )
        finally:
            await runtime.stop()

        assert response["ok"] is True
        assert response["reply"] == "echo:hello"
        assert response["session_id"] == "webhook-dev:local:demo"

    asyncio.run(run())


def test_generic_webhook_records_unclaimed_outbound_after_timeout(tmp_path) -> None:
    async def run() -> None:
        bus = MessageBus()
        runtime = ChannelRuntime(
            service=SlowFakeAgentService(),
            bus=bus,
            workspace=tmp_path,
            channels={},
        )
        adapter = GenericWebhookAdapter(
            channel_id="webhook-dev",
            kind="webhook",
            mode="webhook",
            account_id="local",
            display_name="Webhook Dev",
            inbound_sink=runtime,
            response_timeout_seconds=1,
        )
        adapter.response_timeout_seconds = 0.01
        runtime.manager.register(adapter)
        await runtime.start()
        try:
            response = await adapter.handle_webhook_payload(
                {
                    "peer_id": "demo",
                    "message_id": "msg-1",
                    "text": "hello",
                    "peer_type": "dm",
                }
            )
            await asyncio.sleep(0.1)
            events = runtime.recent_events("webhook-dev", limit=20)
        finally:
            await runtime.stop()

        assert response["pending"] is True
        assert any(event["kind"] == "outbound_unclaimed" for event in events)

    asyncio.run(run())


def test_channel_runtime_starts_enabled_generic_webhook_and_reports_status(tmp_path) -> None:
    async def run() -> None:
        runtime = ChannelRuntime(
            service=FakeAgentService(),
            workspace=tmp_path,
            channels={
                "webhook-dev": ChannelConfig(
                    enabled=True,
                    kind="webhook",
                    mode="webhook",
                    account_id="local",
                    display_name="Webhook Dev",
                    config={"response_timeout_seconds": 1800},
                ),
                "off": ChannelConfig(
                    enabled=False,
                    kind="webhook",
                    mode="webhook",
                    account_id="local",
                ),
            },
        )
        await runtime.start()
        try:
            statuses = runtime.statuses()
        finally:
            await runtime.stop()

        by_id = {item["channel_id"]: item for item in statuses}
        assert by_id["webhook-dev"]["state"] == "running"
        assert by_id["webhook-dev"]["webhook_url"] == "/api/channels/webhook-dev/webhook"
        assert by_id["off"]["state"] == "disabled"

    asyncio.run(run())


def test_channel_runtime_builds_platform_adapters_without_starting_networks(tmp_path) -> None:
    runtime = ChannelRuntime(
        service=FakeAgentService(),
        workspace=tmp_path,
        channels={},
    )

    cases = {
        "telegram-main": ChannelConfig(enabled=True, kind="telegram", mode="polling", account_id="bot-main"),
        "feishu-main": ChannelConfig(enabled=True, kind="feishu", mode="websocket", account_id="tenant-main"),
        "qq-main": ChannelConfig(enabled=True, kind="qqbot", mode="websocket", account_id="qq-main"),
        "weixin-main": ChannelConfig(enabled=True, kind="weixin", mode="polling", account_id="wx-main"),
    }

    for channel_id, cfg in cases.items():
        adapter = runtime._build_adapter(channel_id, cfg)
        assert adapter.channel_id == channel_id
        assert adapter.kind == cfg.kind
        assert adapter.mode == cfg.mode


def test_channel_runtime_reports_platform_capabilities(tmp_path) -> None:
    runtime = ChannelRuntime(
        service=FakeAgentService(),
        workspace=tmp_path,
        channels={
            "telegram-main": ChannelConfig(enabled=True, kind="telegram", mode="polling", account_id="bot-main"),
            "weixin-main": ChannelConfig(enabled=True, kind="weixin", mode="polling", account_id="wx-main"),
        },
    )

    by_id = {item["channel_id"]: item for item in runtime.statuses()}

    assert by_id["telegram-main"]["capabilities"] == [
        "receive_text",
        "send_text",
        "receive_media",
        "groups",
    ]
    assert by_id["weixin-main"]["capabilities"] == [
        "receive_text",
        "send_text",
        "receive_media",
        "direct_messages",
    ]


def test_channel_runtime_platform_start_failure_does_not_stop_other_channels(tmp_path) -> None:
    async def run() -> None:
        runtime = ChannelRuntime(
            service=FakeAgentService(),
            workspace=tmp_path,
            channels={
                "telegram-main": ChannelConfig(
                    enabled=True,
                    kind="telegram",
                    mode="polling",
                    account_id="bot-main",
                    secrets={},
                ),
                "off": ChannelConfig(
                    enabled=False,
                    kind="weixin",
                    mode="polling",
                    account_id="wx-main",
                ),
            },
        )

        await runtime.start()
        try:
            by_id = {item["channel_id"]: item for item in runtime.statuses()}
        finally:
            await runtime.stop()

        assert by_id["telegram-main"]["state"] == "error"
        assert "botToken" in by_id["telegram-main"]["last_error"]
        assert by_id["off"]["state"] == "disabled"

    asyncio.run(run())


def test_web_app_status_exposes_configured_channel(tmp_path) -> None:
    config_path = tmp_path / "config.json"
    workspace = tmp_path / "workspace"
    workspace.mkdir()
    config_path.write_text(
        json.dumps(
            {
                "agents": {"defaults": {"workspace": str(workspace), "model": "openai/gpt-5"}},
                "providers": {},
                "channels": {
                    "webhook-dev": {
                        "enabled": True,
                        "kind": "webhook",
                        "mode": "webhook",
                        "accountId": "local",
                        "displayName": "Webhook Dev",
                    }
                },
            }
        ),
        encoding="utf-8",
    )
    service = AgentService(config_path=config_path)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        payload = client.get("/api/status").json()

    service.close()
    assert payload["channels"][0]["channel_id"] == "webhook-dev"
    assert payload["channels"][0]["state"] == "running"
    assert payload["channels"][0]["webhook_url"] == "/api/channels/webhook-dev/webhook"
    assert payload["runtime_controls"]["self_restart"] is True


def test_self_restart_env_defaults_enabled(monkeypatch) -> None:
    monkeypatch.delenv("BEAVER_ENABLE_SELF_RESTART", raising=False)

    assert _self_restart_enabled() is True


def test_self_restart_env_can_disable(monkeypatch) -> None:
    monkeypatch.setenv("BEAVER_ENABLE_SELF_RESTART", "0")

    assert _self_restart_enabled() is False
