from __future__ import annotations

import logging
from typing import Any
from unittest.mock import MagicMock

import httpx
import pytest

from witty_service.api.services import ServiceContainer
from witty_service.domain.enums import AgentStatus
from witty_service.domain.errors import DomainError
from witty_service.persistence.db import create_session_factory, create_sqlite_engine, init_db
from witty_service.persistence.repositories import SqliteRepository


@pytest.fixture()
def repo() -> SqliteRepository:
    engine = create_sqlite_engine("sqlite:///:memory:")
    init_db(engine)
    factory = create_session_factory(engine)
    try:
        yield SqliteRepository(factory)
    finally:
        engine.dispose()


def _create_agent(repo: SqliteRepository, agent_id: str, name: str) -> None:
    repo.create_agent_with_id(
        agent_id=agent_id,
        name=name,
        description=f"{name} description",
        sandbox_type="local_process",
        adapter_type="http",
        workspace_path=f"/tmp/{agent_id}",
        idle_timeout_seconds=300,
        status=AgentStatus.running,
    )


def _create_session(
    repo: SqliteRepository,
    *,
    agent_id: str,
    session_id: str,
    runtime_session_id: str | None = None,
) -> None:
    repo.upsert_session(
        session_id=session_id,
        agent_id=agent_id,
        status="idle",
        runtime_type="openclaw",
        runtime_session_key=f"agent:{agent_id}:session:{session_id}",
        remote_runtime_agent_id=f"runtime-agent:{agent_id}",
    )
    if runtime_session_id is not None:
        repo.update_session_runtime_identity(
            session_id=session_id,
            runtime_type="openclaw",
            runtime_session_id=runtime_session_id,
            runtime_session_key=f"agent:{agent_id}:session:{session_id}",
        )


def _http_status_error(method: str, path: str, status_code: int) -> httpx.HTTPStatusError:
    request = httpx.Request(method, f"http://localhost:7396{path}")
    response = httpx.Response(status_code, request=request, text="bad gateway")
    return httpx.HTTPStatusError("upstream failure", request=request, response=response)


class FakeInsightHttpClient:
    def __init__(self) -> None:
        self.calls: list[tuple[str, str, Any]] = []
        self.get_results: dict[str, Any] = {"/health": {"ok": True}}
        self.post_results: dict[str, Any] = {}
        self.delete_results: dict[str, Any] = {}
        self.get_errors: dict[str, Exception] = {}
        self.post_errors: dict[str, Exception] = {}
        self.delete_errors: dict[str, Exception] = {}
        self.base_url = "http://localhost:7396"
        self._timeout = 5.0

    async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
        self.calls.append(("GET", path, params))
        error = self.get_errors.get(path)
        if error is not None:
            raise error
        return self.get_results.get(path)

    async def post(
        self,
        path: str,
        json: dict[str, Any] | None = None,
        timeout: float | None = None,
    ) -> Any:
        self.calls.append(("POST", path, {"json": json, "timeout": timeout}))
        error = self.post_errors.get(path)
        if error is not None:
            raise error
        return self.post_results.get(path)

    async def delete(self, path: str) -> Any:
        self.calls.append(("DELETE", path, None))
        error = self.delete_errors.get(path)
        if error is not None:
            raise error
        return self.delete_results.get(path)


def _make_facade(
    repo: SqliteRepository,
    insight_http_client: FakeInsightHttpClient | None = None,
):
    from witty_service.application.insight_facade import InsightFacade

    return InsightFacade(
        ServiceContainer(
            repository=repo,
            workspace_store=MagicMock(),
            insight_http_client=insight_http_client,
        ),
    )


def test_raw_params_uses_bracketed_session_ids_for_multiple_runtime_sessions() -> None:
    from witty_service.application.insight_facade import InsightFacade

    assert InsightFacade._raw_params(session_ids=["runtime-1", "runtime-2"]) == [
        ("session_ids[]", "runtime-1"),
        ("session_ids[]", "runtime-2"),
    ]


def test_raw_params_keeps_singular_session_id_for_single_runtime_session() -> None:
    from witty_service.application.insight_facade import InsightFacade

    assert InsightFacade._raw_params(session_ids=["runtime-1"]) == {
        "session_id": "runtime-1",
    }


@pytest.mark.asyncio
async def test_get_capabilities_reports_unreachable_when_health_probe_fails(
    repo: SqliteRepository,
) -> None:
    insight_http_client = FakeInsightHttpClient()
    insight_http_client.get_errors["/health"] = httpx.ConnectError("connection refused")
    facade = _make_facade(repo, insight_http_client)

    assert await facade.get_capabilities() == {
        "enabled": True,
        "reachable": False,
        "features": {
            "sessions": True,
            "timeseries": True,
            "interruptions": True,
            "health": True,
        },
    }


@pytest.mark.asyncio
async def test_list_sessions_enriches_managed_sessions_and_warns_for_missing_links(
    repo: SqliteRepository,
    caplog: pytest.LogCaptureFixture,
) -> None:
    _create_agent(repo, "agent-1", "Alpha")
    _create_agent(repo, "agent-2", "Beta")
    _create_session(repo, agent_id="agent-1", session_id="session-1", runtime_session_id="runtime-1")
    _create_session(repo, agent_id="agent-2", session_id="session-2", runtime_session_id="runtime-2")
    repo.list_agent_records_by_ids = MagicMock(
        return_value=repo.list_agent_records_by_ids(["agent-1"])
    )

    insight_http_client = FakeInsightHttpClient()
    insight_http_client.get_results["/api/sessions"] = [
        {
            "session_id": "runtime-1",
            "conversation_count": 3,
            "first_seen_ns": 100,
            "last_seen_ns": 300,
            "total_input_tokens": 11,
            "total_output_tokens": 7,
            "model": "gpt-4o",
            "agent_name": "raw-alpha",
        },
        {
            "session_id": "runtime-2",
            "conversation_count": 2,
            "first_seen_ns": 200,
            "last_seen_ns": 400,
            "total_input_tokens": 5,
            "total_output_tokens": 6,
            "model": "gpt-4o-mini",
            "agent_name": "raw-beta",
        },
        {
            "session_id": "runtime-missing",
            "conversation_count": 1,
            "first_seen_ns": 500,
            "last_seen_ns": 600,
            "total_input_tokens": 2,
            "total_output_tokens": 3,
            "model": "gpt-4.1",
        },
    ]
    facade = _make_facade(repo, insight_http_client)

    with caplog.at_level(logging.WARNING):
        result = await facade.list_sessions()

    assert insight_http_client.calls[-1] == (
        "GET",
        "/api/sessions",
        [
            ("session_ids[]", "runtime-1"),
            ("session_ids[]", "runtime-2"),
        ],
    )
    assert result == [
        {
            "session_id": "session-1",
            "runtime_session_id": "runtime-1",
            "witty_agent_id": "agent-1",
            "witty_agent_name": "Alpha",
            "agent_name": "Alpha",
            "conversation_count": 3,
            "first_seen_ns": 100,
            "last_seen_ns": 300,
            "total_input_tokens": 11,
            "total_output_tokens": 7,
            "model": "gpt-4o",
        }
    ]
    assert "runtime session is missing a local witty session mapping" in caplog.text
    assert "mapped witty session references a missing agent" in caplog.text


@pytest.mark.asyncio
async def test_get_session_traces_uses_witty_session_runtime_mapping(
    repo: SqliteRepository,
) -> None:
    _create_agent(repo, "agent-1", "Alpha")
    _create_session(repo, agent_id="agent-1", session_id="session-1", runtime_session_id="runtime-1")
    insight_http_client = FakeInsightHttpClient()
    insight_http_client.get_results["/api/sessions/runtime-1/traces"] = [{"trace_id": "trace-1"}]
    facade = _make_facade(repo, insight_http_client)

    result = await facade.get_session_traces("session-1", start_ns=10, end_ns=20)

    assert insight_http_client.calls[-1] == (
        "GET",
        "/api/sessions/runtime-1/traces",
        {"start_ns": 10, "end_ns": 20},
    )
    assert result == [{"trace_id": "trace-1"}]


@pytest.mark.asyncio
async def test_get_session_traces_raises_when_runtime_mapping_is_missing(
    repo: SqliteRepository,
) -> None:
    _create_agent(repo, "agent-1", "Alpha")
    _create_session(repo, agent_id="agent-1", session_id="session-1")
    facade = _make_facade(repo, FakeInsightHttpClient())

    with pytest.raises(DomainError) as exc_info:
        await facade.get_session_traces("session-1")

    assert exc_info.value.code == "INSIGHT_SESSION_MAPPING_NOT_FOUND"


@pytest.mark.asyncio
async def test_get_session_interruptions_remaps_runtime_session_ids(
    repo: SqliteRepository,
) -> None:
    _create_agent(repo, "agent-1", "Alpha")
    _create_session(repo, agent_id="agent-1", session_id="session-1", runtime_session_id="runtime-1")
    insight_http_client = FakeInsightHttpClient()
    insight_http_client.get_results["/api/sessions/runtime-1/interruptions"] = [
        {
            "interruption_id": "interrupt-1",
            "session_id": "runtime-1",
            "severity": "critical",
        }
    ]
    facade = _make_facade(repo, insight_http_client)

    result = await facade.get_session_interruptions("session-1")

    assert result == [
        {
            "interruption_id": "interrupt-1",
            "session_id": "session-1",
            "runtime_session_id": "runtime-1",
            "severity": "critical",
        }
    ]


@pytest.mark.asyncio
async def test_get_timeseries_and_interruption_count_return_empty_without_managed_sessions(
    repo: SqliteRepository,
) -> None:
    facade = _make_facade(repo, FakeInsightHttpClient())

    assert await facade.get_timeseries() == {"token_series": [], "model_series": []}
    assert await facade.get_interruption_count() == {
        "total": 0,
        "by_severity": {"critical": 0, "high": 0, "medium": 0, "low": 0},
    }


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("method_name", "args", "http_method", "path", "error", "expected_code"),
    [
        (
            "get_trace_detail",
            ("trace-1",),
            "GET",
            "/api/traces/trace-1",
            httpx.ConnectError("boom"),
            "INSIGHT_UNAVAILABLE",
        ),
        (
            "resolve_interruption",
            ("interrupt-1",),
            "POST",
            "/api/interruptions/interrupt-1/resolve",
            httpx.ReadTimeout("slow"),
            "INSIGHT_TIMEOUT",
        ),
        (
            "delete_agent_health",
            (101,),
            "DELETE",
            "/api/agent-health/101",
            _http_status_error("DELETE", "/api/agent-health/101", 503),
            "INSIGHT_UPSTREAM_ERROR",
        ),
        (
            "restart_agent_health",
            (101,),
            "POST",
            "/api/agent-health/101/restart",
            ValueError("invalid json"),
            "INSIGHT_BAD_RESPONSE",
        ),
    ],
)
async def test_insight_http_errors_are_mapped_to_domain_errors(
    repo: SqliteRepository,
    method_name: str,
    args: tuple[Any, ...],
    http_method: str,
    path: str,
    error: Exception,
    expected_code: str,
) -> None:
    insight_http_client = FakeInsightHttpClient()
    if http_method == "GET":
        insight_http_client.get_errors[path] = error
    elif http_method == "POST":
        insight_http_client.post_errors[path] = error
    else:
        insight_http_client.delete_errors[path] = error

    facade = _make_facade(repo, insight_http_client)

    with pytest.raises(DomainError) as exc_info:
        await getattr(facade, method_name)(*args)

    assert exc_info.value.code == expected_code
    assert exc_info.value.details["path"] == path
    assert exc_info.value.details["base_url"] == "http://localhost:7396"


@pytest.mark.asyncio
async def test_export_atif_session_rewrites_session_identity(
    repo: SqliteRepository,
) -> None:
    _create_agent(repo, "agent-1", "Alpha")
    _create_session(repo, agent_id="agent-1", session_id="session-1", runtime_session_id="runtime-1")
    insight_http_client = FakeInsightHttpClient()
    insight_http_client.get_results["/api/export/atif/session/runtime-1"] = {
        "schema_version": "1.6",
        "session_id": "runtime-1",
        "agent": {},
        "steps": [],
    }
    facade = _make_facade(repo, insight_http_client)

    result = await facade.export_atif_session("session-1")

    assert result == {
        "schema_version": "1.6",
        "session_id": "session-1",
        "runtime_session_id": "runtime-1",
        "agent": {},
        "steps": [],
    }


@pytest.mark.asyncio
async def test_get_agent_health_joins_managed_agents_and_orphan_runtimes(
    repo: SqliteRepository,
) -> None:
    _create_agent(repo, "agent-1", "Alpha")
    _create_agent(repo, "agent-2", "Beta")
    repo.save_sandbox_state(
        "agent-1",
        sandbox_payload_json={
            "sandbox_id": "sandbox-1",
            "workspace_path": "/tmp/agent-1",
            "metadata": {"gateway_port": 18080, "stderr_log_path": "/tmp/alpha.err"},
        },
        adapter_base_url="http://127.0.0.1:18080",
        adapter_ready=True,
    )
    repo.save_sandbox_state(
        "agent-2",
        sandbox_payload_json={
            "sandbox_id": "sandbox-2",
            "workspace_path": "/tmp/agent-2",
            "metadata": {"gateway_port": 18081},
        },
        adapter_base_url="http://127.0.0.1:18081",
        adapter_ready=False,
    )

    insight_http_client = FakeInsightHttpClient()
    insight_http_client.get_results["/api/agent-health"] = {
        "agents": [
            {
                "pid": 101,
                "agent_name": "raw-alpha",
                "category": "openclaw",
                "exe_path": "/usr/bin/openclaw",
                "ports": [18080],
                "status": "healthy",
                "last_check_time": 111,
                "latency_ms": 12,
                "error_message": None,
            },
            {
                "pid": 202,
                "agent_name": "orphan-runtime",
                "category": "openclaw",
                "exe_path": "/usr/bin/openclaw",
                "ports": [19999],
                "status": "offline",
                "last_check_time": 222,
                "latency_ms": None,
                "error_message": "exited",
            },
        ],
        "last_scan_time": 999,
    }
    facade = _make_facade(repo, insight_http_client)

    result = await facade.get_agent_health()

    assert result["last_scan_time"] == 999
    assert result["agents"] == [
        {
            "witty_agent_id": "agent-1",
            "witty_agent_name": "Alpha",
            "witty_status": "running",
            "overall_status": "healthy",
            "status_reason": None,
            "adapter_type": "http",
            "sandbox_type": "local_process",
            "workspace_path": "/tmp/agent-1",
            "gateway_port": 18080,
            "adapter_base_url": "http://127.0.0.1:18080",
            "adapter_ready": True,
            "adapter_status": "ready",
            "adapter_latency_ms": 12,
            "adapter_error_message": None,
            "adapter_pid": 101,
            "stderr_log_path": "/tmp/alpha.err",
            "runtime": {
                "pid": 101,
                "agent_name": "raw-alpha",
                "category": "openclaw",
                "exe_path": "/usr/bin/openclaw",
                "ports": [18080],
                "status": "healthy",
                "last_check_time": 111,
                "latency_ms": 12,
                "error_message": None,
            },
            "candidate_runtimes": [],
        },
        {
            "witty_agent_id": "agent-2",
            "witty_agent_name": "Beta",
            "witty_status": "running",
            "overall_status": "missing_runtime",
            "status_reason": "No matching runtime health entry found.",
            "adapter_type": "http",
            "sandbox_type": "local_process",
            "workspace_path": "/tmp/agent-2",
            "gateway_port": 18081,
            "adapter_base_url": "http://127.0.0.1:18081",
            "adapter_ready": False,
            "adapter_status": "not_ready",
            "adapter_latency_ms": None,
            "adapter_error_message": None,
            "adapter_pid": None,
            "stderr_log_path": None,
            "runtime": None,
            "candidate_runtimes": [],
        },
    ]
    assert result["orphan_runtimes"] == [
        {
            "pid": 202,
            "agent_name": "orphan-runtime",
            "category": "openclaw",
            "exe_path": "/usr/bin/openclaw",
            "ports": [19999],
            "status": "offline",
            "last_check_time": 222,
            "latency_ms": None,
            "error_message": "exited",
        }
    ]


@pytest.mark.asyncio
async def test_get_agent_health_prefers_live_runtime_over_stale_offline_matches(
    repo: SqliteRepository,
) -> None:
    _create_agent(repo, "agent-1", "Alpha")
    repo.save_sandbox_state(
        "agent-1",
        sandbox_payload_json={
            "sandbox_id": "sandbox-1",
            "workspace_path": "/tmp/agent-1",
            "metadata": {"gateway_port": 18080, "stderr_log_path": "/tmp/alpha.err"},
        },
        adapter_base_url="http://127.0.0.1:18080",
        adapter_ready=True,
    )

    insight_http_client = FakeInsightHttpClient()
    insight_http_client.get_results["/api/agent-health"] = {
        "agents": [
            {
                "pid": 101,
                "agent_name": "raw-alpha-live",
                "category": "openclaw",
                "exe_path": "/usr/bin/openclaw",
                "ports": [18080],
                "status": "healthy",
                "last_check_time": 300,
                "latency_ms": 8,
                "error_message": None,
            },
            {
                "pid": 99,
                "agent_name": "raw-alpha-old",
                "category": "openclaw",
                "exe_path": "/usr/bin/openclaw",
                "ports": [18080],
                "status": "offline",
                "last_check_time": 200,
                "latency_ms": None,
                "error_message": "exited",
            },
        ],
        "last_scan_time": 1234,
    }
    facade = _make_facade(repo, insight_http_client)

    result = await facade.get_agent_health()
    agent = result["agents"][0]

    assert agent["overall_status"] == "healthy"
    assert agent["status_reason"] is None
    assert agent["adapter_pid"] == 101
    assert agent["runtime"] == {
        "pid": 101,
        "agent_name": "raw-alpha-live",
        "category": "openclaw",
        "exe_path": "/usr/bin/openclaw",
        "ports": [18080],
        "status": "healthy",
        "last_check_time": 300,
        "latency_ms": 8,
        "error_message": None,
    }
    assert agent["candidate_runtimes"] == []