"""Owner-scope end-to-end scenarios.

These tests exercise the explicit owner model across:
- the web gateway chat UI
- the owner-scoped HTTP webhook channel
- routine tools / routines tab
"""

import asyncio
import json
import uuid

import httpx

from helpers import (
    AUTH_TOKEN,
    SEL,
    api_get,
    api_post,
    signed_http_webhook_headers,
)


async def _send_and_get_response(
    page,
    message: str,
    *,
    expected_fragment: str,
    timeout: int = 30000,
) -> str:
    """Send a chat message and return the newest assistant response text."""
    chat_input = page.locator(SEL["chat_input"])
    await chat_input.wait_for(state="visible", timeout=5000)

    assistant_sel = SEL["message_assistant"]
    before_count = await page.locator(assistant_sel).count()

    await chat_input.fill(message)
    await chat_input.press("Enter")

    expected = before_count + 1
    await page.wait_for_function(
        """({ assistantSelector, expectedCount, expectedFragment }) => {
            const messages = document.querySelectorAll(assistantSelector);
            if (messages.length < expectedCount) return false;
            const text = (messages[messages.length - 1].innerText || '').trim().toLowerCase();
            return text.includes(expectedFragment.toLowerCase());
        }""",
        arg={
            "assistantSelector": assistant_sel,
            "expectedCount": expected,
            "expectedFragment": expected_fragment,
        },
        timeout=timeout,
    )

    return await page.locator(assistant_sel).last.inner_text()


async def _post_http_webhook(
    http_channel_server: str,
    *,
    content: str,
    sender_id: str,
    thread_id: str,
    wait_for_response: bool = True,
) -> str | None:
    """Send a signed request to the owner-scoped HTTP webhook channel."""
    payload = {
        "user_id": sender_id,
        "thread_id": thread_id,
        "content": content,
        "wait_for_response": wait_for_response,
    }
    body = json.dumps(payload).encode("utf-8")

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{http_channel_server}/webhook",
            content=body,
            headers=signed_http_webhook_headers(body),
            timeout=90,
        )

    assert response.status_code == 200, (
        f"HTTP webhook failed: {response.status_code} {response.text[:400]}"
    )
    data = response.json()
    assert data["status"] == "accepted", f"Unexpected webhook response: {data}"
    if wait_for_response:
        assert data["response"], f"Expected synchronous response body, got: {data}"
    return data.get("response")


async def _open_tab(page, tab: str) -> None:
    btn = page.locator(SEL["tab_button"].format(tab=tab))
    await btn.click()
    await page.locator(SEL["tab_panel"].format(tab=tab)).wait_for(
        state="visible",
        timeout=5000,
    )


async def _wait_for_routine(base_url: str, name: str, timeout: float = 20.0) -> dict:
    """Poll the routines API until the named routine exists."""
    async with httpx.AsyncClient() as client:
        for _ in range(int(timeout * 2)):
            response = await client.get(
                f"{base_url}/api/routines",
                headers={"Authorization": f"Bearer {AUTH_TOKEN}"},
                timeout=10,
            )
            response.raise_for_status()
            routines = response.json()["routines"]
            for routine in routines:
                if routine["name"] == name:
                    return routine
            await _poll_sleep()
    raise AssertionError(f"Routine '{name}' was not created within {timeout}s")


async def _wait_for_http_thread(base_url: str, title_fragment: str, timeout: float = 20.0) -> str:
    """Poll the chat thread list until the matching HTTP thread is visible."""
    for _ in range(int(timeout * 2)):
        response = await api_get(base_url, "/api/chat/threads", timeout=10)
        response.raise_for_status()
        threads = response.json()["threads"]
        for thread in threads:
            if thread.get("channel") != "http":
                continue
            if title_fragment in (thread.get("title") or ""):
                return thread["id"]
        await _poll_sleep()
    raise AssertionError(
        f"HTTP thread containing '{title_fragment}' was not visible within {timeout}s"
    )


async def _wait_for_pending_approval(
    base_url: str,
    thread_id: str,
    timeout: float = 20.0,
) -> dict:
    """Poll chat history until the thread exposes a pending approval payload."""
    for _ in range(int(timeout * 2)):
        response = await api_get(
            base_url,
            f"/api/chat/history?thread_id={thread_id}",
            timeout=10,
        )
        response.raise_for_status()
        pending = response.json().get("pending_approval")
        if pending:
            return pending
        await _poll_sleep()
    raise AssertionError(f"Thread '{thread_id}' did not expose a pending approval")


async def _approve_pending_request(base_url: str, thread_id: str, request_id: str) -> None:
    """Approve a pending tool request through the web gateway API."""
    response = await api_post(
        base_url,
        "/api/chat/approval",
        json={
            "request_id": request_id,
            "action": "approve",
            "thread_id": thread_id,
        },
        timeout=10,
    )
    assert response.status_code == 202, (
        f"Approval submission failed: {response.status_code} {response.text[:400]}"
    )
    data = response.json()
    assert data["status"] == "accepted", f"Unexpected approval response: {data}"


async def _poll_sleep() -> None:
    """Small shared backoff for API polling loops."""
    await asyncio.sleep(0.5)


async def test_http_channel_created_routine_is_visible_in_web_routines_tab(
    page,
    ironclaw_server,
    http_channel_server,
):
    """A routine created from the HTTP channel is visible in the web owner UI."""
    routine_name = f"owner-http-{uuid.uuid4().hex[:8]}"

    response_text = await _post_http_webhook(
        http_channel_server,
        content=f"create lightweight owner routine {routine_name}",
        sender_id="external-sender-alpha",
        thread_id="http-owner-routine-thread",
    )
    assert routine_name in response_text

    await _wait_for_routine(ironclaw_server, routine_name)

    await _open_tab(page, "routines")
    await page.locator(SEL["routine_row"]).filter(has_text=routine_name).first.wait_for(
        state="visible",
        timeout=15000,
    )


async def test_web_created_routine_is_listed_from_http_channel_across_senders(
    page,
    ironclaw_server,
    http_channel_server,
):
    """Routines created in web chat remain owner-global across HTTP senders/threads."""
    routine_name = f"owner-web-{uuid.uuid4().hex[:8]}"

    assistant_text = await _send_and_get_response(
        page,
        f"create lightweight owner routine {routine_name}",
        expected_fragment=routine_name,
    )
    assert routine_name in assistant_text

    await _wait_for_routine(ironclaw_server, routine_name)

    first_sender_text = await _post_http_webhook(
        http_channel_server,
        content="list owner routines",
        sender_id="http-sender-one",
        thread_id="owner-list-thread-a",
    )
    second_sender_text = await _post_http_webhook(
        http_channel_server,
        content="list owner routines",
        sender_id="http-sender-two",
        thread_id="owner-list-thread-b",
    )

    assert routine_name in first_sender_text, first_sender_text
    assert routine_name in second_sender_text, second_sender_text


async def test_http_created_full_job_routine_is_visible_in_web_after_approval(
    page,
    ironclaw_server,
    http_channel_server,
):
    """A full-job routine created via HTTP appears in the web owner UI after approval."""
    routine_name = f"owner-job-{uuid.uuid4().hex[:8]}"

    await _post_http_webhook(
        http_channel_server,
        content=f"create full-job owner routine {routine_name}",
        sender_id="http-job-sender",
        thread_id="owner-job-thread",
        wait_for_response=False,
    )

    thread_id = await _wait_for_http_thread(ironclaw_server, routine_name)
    pending = await _wait_for_pending_approval(ironclaw_server, thread_id)
    assert pending["tool_name"] == "routine_create"
    await _approve_pending_request(
        ironclaw_server,
        thread_id,
        pending["request_id"],
    )

    routine = await _wait_for_routine(ironclaw_server, routine_name)
    assert routine["action_type"] == "full_job"

    await _open_tab(page, "routines")
    routine_row = page.locator(SEL["routine_row"]).filter(has_text=routine_name).first
    await routine_row.wait_for(state="visible", timeout=15000)
