import asyncio
import json
import time
from pathlib import Path

from fastapi.testclient import TestClient

from beaver.foundation.events import InboundMessage, OutboundMessage
from beaver.interfaces.web.app import create_app
from beaver.services.agent_service import AgentService


class TerminalFakeAgentService(AgentService):
    def __init__(self, *, config_path: Path, delay_seconds: float = 0.0) -> None:
        super().__init__(config_path=config_path)
        self.delay_seconds = delay_seconds
        self.inbound_calls: list[InboundMessage] = []

    async def handle_inbound_message(self, inbound: InboundMessage) -> OutboundMessage:
        self.inbound_calls.append(inbound)
        if self.delay_seconds:
            await asyncio.sleep(self.delay_seconds)
        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,
        )


def write_terminal_config(tmp_path: Path) -> Path:
    workspace = tmp_path / "workspace"
    workspace.mkdir()
    config_path = tmp_path / "config.json"
    config_path.write_text(
        json.dumps(
            {
                "agents": {"defaults": {"workspace": str(workspace), "model": "openai/gpt-5"}},
                "providers": {},
                "channels": {
                    "terminal-dev": {
                        "enabled": True,
                        "kind": "terminal",
                        "mode": "websocket",
                        "accountId": "local",
                        "displayName": "Terminal Dev",
                        "config": {"heartbeatSeconds": 30, "maxMessageChars": 20000},
                    }
                },
            }
        ),
        encoding="utf-8",
    )
    return config_path


def test_terminal_websocket_connect_ping_and_message_roundtrip(tmp_path: Path) -> None:
    config_path = write_terminal_config(tmp_path)
    service = TerminalFakeAgentService(config_path=config_path)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
            websocket.send_json(
                {
                    "type": "connect",
                    "peer_id": "device-001",
                    "device_name": "desk-terminal",
                    "capabilities": ["text"],
                }
            )
            assert websocket.receive_json() == {
                "type": "connected",
                "channel_id": "terminal-dev",
                "session_id": "terminal-dev:local:device-001",
            }

            websocket.send_json({"type": "ping"})
            assert websocket.receive_json() == {"type": "pong"}

            websocket.send_json(
                {
                    "type": "message",
                    "message_id": "device-001-000001",
                    "text": "hello",
                }
            )
            assert websocket.receive_json() == {
                "type": "ack",
                "message_id": "device-001-000001",
                "session_id": "terminal-dev:local:device-001",
                "accepted": True,
            }
            reply = websocket.receive_json()

    service.close()
    assert reply == {
        "type": "message",
        "role": "assistant",
        "message_id": "device-001-000001",
        "run_id": "run-1",
        "text": "echo:hello",
        "finish_reason": "stop",
    }
    assert len(service.inbound_calls) == 1
    inbound = service.inbound_calls[0]
    assert inbound.channel == "terminal-dev"
    assert inbound.content == "hello"
    assert inbound.content_type == "text"
    assert inbound.session_id == "terminal-dev:local:device-001"
    assert inbound.channel_identity is not None
    assert inbound.channel_identity.peer_id == "device-001"
    assert inbound.channel_identity.peer_type == "terminal"
    assert inbound.channel_identity.message_id == "device-001-000001"


def test_terminal_websocket_rejects_message_before_connect(tmp_path: Path) -> None:
    config_path = write_terminal_config(tmp_path)
    service = TerminalFakeAgentService(config_path=config_path)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
            websocket.send_json({"type": "message", "message_id": "m1", "text": "hello"})
            assert websocket.receive_json() == {
                "type": "error",
                "error": "connect is required before message",
            }
            websocket.send_json({"type": "ping"})
            assert websocket.receive_json() == {"type": "pong"}

    service.close()
    assert service.inbound_calls == []


def test_terminal_websocket_unknown_frame_keeps_connection_open(tmp_path: Path) -> None:
    config_path = write_terminal_config(tmp_path)
    service = TerminalFakeAgentService(config_path=config_path)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
            websocket.send_json({"type": "example"})
            assert websocket.receive_json() == {
                "type": "error",
                "error": "Unsupported websocket frame type: example",
            }
            websocket.send_json({"type": "ping"})
            assert websocket.receive_json() == {"type": "pong"}

    service.close()


def test_terminal_websocket_validates_message_fields(tmp_path: Path) -> None:
    config_path = write_terminal_config(tmp_path)
    service = TerminalFakeAgentService(config_path=config_path)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
            websocket.send_json({"type": "connect", "peer_id": "device-001"})
            assert websocket.receive_json()["type"] == "connected"

            websocket.send_json({"type": "message", "text": "hello"})
            assert websocket.receive_json() == {"type": "error", "error": "message_id is required"}

            websocket.send_json({"type": "message", "message_id": "m1", "text": "   "})
            assert websocket.receive_json() == {"type": "error", "error": "text is required"}

    service.close()
    assert service.inbound_calls == []


def test_terminal_websocket_duplicate_message_returns_cached_reply(tmp_path: Path) -> None:
    config_path = write_terminal_config(tmp_path)
    service = TerminalFakeAgentService(config_path=config_path)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
            websocket.send_json({"type": "connect", "peer_id": "device-001"})
            assert websocket.receive_json()["type"] == "connected"

            frame = {"type": "message", "message_id": "device-001-000001", "text": "hello"}
            websocket.send_json(frame)
            assert websocket.receive_json()["accepted"] is True
            assert websocket.receive_json()["text"] == "echo:hello"

            websocket.send_json(frame)
            duplicate = websocket.receive_json()

    service.close()
    assert duplicate["type"] == "ack"
    assert duplicate["accepted"] is False
    assert duplicate["duplicate"] is True
    assert duplicate["pending"] is False
    assert duplicate["reply"] == "echo:hello"
    assert len(service.inbound_calls) == 1


def test_terminal_websocket_disconnect_before_reply_records_unclaimed(tmp_path: Path) -> None:
    config_path = write_terminal_config(tmp_path)
    service = TerminalFakeAgentService(config_path=config_path, delay_seconds=0.05)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
            websocket.send_json({"type": "connect", "peer_id": "device-001"})
            assert websocket.receive_json()["type"] == "connected"
            websocket.send_json({"type": "message", "message_id": "device-001-000001", "text": "slow"})
            assert websocket.receive_json()["accepted"] is True

        time.sleep(0.15)
        events = client.get("/api/channels/terminal-dev/events").json()

    service.close()
    kinds = [event["kind"] for event in events]
    assert "terminal_disconnected" in kinds
    assert "outbound_unclaimed" in kinds


def test_terminal_channel_status_exposes_websocket_url_and_peer_count(tmp_path: Path) -> None:
    config_path = write_terminal_config(tmp_path)
    service = TerminalFakeAgentService(config_path=config_path)
    app = create_app(service=service, manage_service_lifecycle=False)

    with TestClient(app) as client:
        initial = client.get("/api/status").json()["channels"][0]
        assert initial["channel_id"] == "terminal-dev"
        assert initial["websocket_url"] == "/api/channels/terminal-dev/ws"
        assert initial["connected_peers"] == 0
        assert "persistent_connection" in initial["capabilities"]

        with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
            websocket.send_json({"type": "connect", "peer_id": "device-001"})
            assert websocket.receive_json()["type"] == "connected"
            connected = client.get("/api/status").json()["channels"][0]
            assert connected["connected_peers"] == 1

    service.close()
