# -*- coding: utf-8 -*-
"""
QwenPaw Inbox page object.

Inbox is a file-backed event store + per-run trace store + an in-memory
approval queue. The first two are seedable on disk so the full UI and
HTTP contract can be exercised without invoking the LLM. Approvals
are intentionally out of scope (no seed surface; would require driving
a real protected-tool call).

Cases covered:
- INBOX-001 P0  test_seeded_events_listed_with_filters
- INBOX-002 P0  test_inbox_page_renders_with_seeded_events
- INBOX-003 P1  test_mark_read_and_delete_cascade_trace
- INBOX-004 P1  test_message_card_renders_and_modal_opens
- INBOX-005 P1  test_batch_mode_select_and_delete
- INBOX-006 P1  test_sidebar_unread_dot_appears_with_seeded_event
- INBOX-007 P2  test_empty_inbox_contract
- INBOX-008 P2  test_delete_missing_event_returns_404
"""
from __future__ import annotations

import json
import logging
import os
import shutil
import time
from pathlib import Path
from typing import List, Optional

from playwright.sync_api import expect, TimeoutError

from pages.base_page import BasePage
from config.settings import config


logger = logging.getLogger(__name__)


class InboxPage(BasePage):
    """Page object + seed helpers for the Inbox module."""

    PAGE_URL = f"{config.base_url}/inbox"
    AGENT_ID_DEFAULT = "default"

    # ========== Selectors ==========

    PAGE_TITLE_TEXT = (
        'h1:has-text("Inbox"), '
        'h1:has-text("收件箱"), '
        '[class*="breadcrumbCurrent"]:has-text("Inbox"), '
        '[class*="breadcrumbCurrent"]:has-text("收件箱")'
    )

    TAB_PUSH = (
        '.qwenpaw-tabs-tab:has-text("Push Messages"), '
        '.qwenpaw-tabs-tab:has-text("推送消息")'
    )
    TAB_APPROVALS = (
        '.qwenpaw-tabs-tab:has-text("Approvals"), '
        '.qwenpaw-tabs-tab:has-text("审批")'
    )

    # CSS Modules class names are hashed but they all carry the
    # original identifier as a substring — `[class*=foo]` is enough.
    MESSAGE_CARD = '[class*="messageCard"]'
    UNREAD_DOT_IN_CARD = '[class*="unreadDot"]'

    # Sidebar unread dot: the inbox menu item carries an antd Badge dot
    # rendered as <sup class="qwenpaw-badge-dot"> inside the menu title.
    # data-menu-id is auto-generated by rc-menu (e.g. "rc-menu-uuid-N-inbox"),
    # so we anchor on the bilingual menu label instead.
    SIDEBAR_INBOX_BADGE = (
        'li.qwenpaw-menu-item:has(span.qwenpaw-menu-title-content:has-text("Inbox")) '
        '.qwenpaw-badge-dot, '
        'li.qwenpaw-menu-item:has(span.qwenpaw-menu-title-content:has-text("收件箱")) '
        '.qwenpaw-badge-dot'
    )

    # Detail modal
    DETAIL_MODAL = '.qwenpaw-modal'

    # Batch toolbar
    BATCH_ENTER_BTN = (
        'button:has-text("Batch"), '
        'button:has-text("批量操作")'
    )
    BATCH_SELECT_ALL = (
        'label:has-text("Select current page"), '
        'label:has-text("全选当前页")'
    )
    BATCH_DELETE_BTN = (
        'button:has-text("Batch Delete"), '
        'button:has-text("批量删除")'
    )
    POPCONFIRM_OK = (
        '.qwenpaw-popconfirm button.qwenpaw-btn-primary, '
        '.qwenpaw-popover button.qwenpaw-btn-primary'
    )

    EMPTY_PUSH = (
        'text=/(No push messages|暂无推送消息)/'
    )

    # ========== Workspace path helpers ==========

    @staticmethod
    def working_dir() -> Path:
        """Return the agent root working dir.

        Delegates to ``config.working_dir`` (single source of truth).
        Page objects must not duplicate the env-var resolution logic;
        if the resolution rules ever change, only ``config`` should
        need updating.
        """
        from config.settings import config
        return config.working_dir

    @classmethod
    def inbox_path(cls) -> Path:
        return cls.working_dir() / "inbox_events.json"

    @classmethod
    def trace_dir(cls) -> Path:
        return cls.working_dir() / "inbox_traces"

    # ========== Seed / clean (file-system) ==========

    @classmethod
    def clean_inbox(cls) -> None:
        """Remove inbox file + trace dir so the next test starts clean."""
        path = cls.inbox_path()
        try:
            if path.exists():
                path.unlink()
        except Exception as exc:  # pragma: no cover
            logger.warning("Failed to remove %s: %s", path, exc)
        directory = cls.trace_dir()
        try:
            if directory.exists():
                shutil.rmtree(directory, ignore_errors=True)
        except Exception as exc:  # pragma: no cover
            logger.warning("Failed to remove %s: %s", directory, exc)

    @classmethod
    def seed_events(cls, events: List[dict]) -> None:
        """Write the events list to inbox_events.json."""
        path = cls.inbox_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(
            json.dumps(events, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        logger.info("Seeded %d inbox events to %s", len(events), path)

    @classmethod
    def seed_trace(cls, run_id: str, payload: dict) -> None:
        """Write one trace file under inbox_traces/<run_id>.json."""
        directory = cls.trace_dir()
        directory.mkdir(parents=True, exist_ok=True)
        (directory / f"{run_id}.json").write_text(
            json.dumps(payload, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

    @staticmethod
    def make_event(
        *,
        event_id: str,
        agent_id: str = "default",
        source_type: str = "cron",
        source_id: str = "",
        event_type: str = "cron_result",
        status: str = "success",
        severity: str = "info",
        title: str = "seeded event",
        body: str = "",
        payload: Optional[dict] = None,
        read: bool = False,
        created_at: Optional[float] = None,
    ) -> dict:
        """Mirror the shape produced by inbox_store.append_event."""
        return {
            "id": event_id,
            "agent_id": agent_id,
            "source_type": source_type,
            "source_id": source_id,
            "event_type": event_type,
            "status": status,
            "severity": severity,
            "title": title,
            "body": body,
            "payload": payload or {},
            "read": read,
            "created_at": (
                created_at if created_at is not None else time.time()
            ),
        }

    # ========== Backend API helpers ==========

    def _agent_headers(self) -> dict:
        return {"X-Agent-Id": self.AGENT_ID_DEFAULT}

    def api_list_events(
        self,
        api_context,
        source_type: str = "",
        unread_only: bool = False,
    ) -> List[dict]:
        params = {}
        if source_type:
            params["source_type"] = source_type
        if unread_only:
            params["unread_only"] = "true"
        resp = api_context.get(
            "/api/console/inbox/events",
            params=params,
            headers=self._agent_headers(),
        )
        assert resp.ok, (
            f"List events failed [{resp.status}]: {resp.text()}"
        )
        body = resp.json()
        return body.get("events", []) if isinstance(body, dict) else []

    # ========== UI helpers ==========

    _init_script_installed = False

    def _install_default_agent_init_script(self) -> None:
        if self._init_script_installed:
            return
        agent = self.AGENT_ID_DEFAULT
        script = (
            "(() => {"
            "  try {"
            f"    const a = '{agent}';"
            "    const blob = JSON.stringify({"
            "      state: { selectedAgent: a, agents: [], lastChatIdByAgent: {} },"
            "      version: 0"
            "    });"
            "    try { localStorage.setItem('qwenpaw-last-used-agent', a); } catch (e) {}"
            "    try { localStorage.setItem('qwenpaw-agent-storage', blob); } catch (e) {}"
            "    try { sessionStorage.setItem('qwenpaw-agent-storage', blob); } catch (e) {}"
            "  } catch (e) {}"
            "})();"
        )
        try:
            self.page.context.add_init_script(script=script)
            self._init_script_installed = True
        except Exception as exc:  # pragma: no cover
            logger.warning("Could not install init script: %s", exc)

    def open(self) -> "InboxPage":
        self._install_default_agent_init_script()
        self.page.goto(self.PAGE_URL, wait_until="commit", timeout=self.timeout)
        try:
            self.page.wait_for_load_state(
                "networkidle", timeout=self.timeout,
            )
        except TimeoutError:
            pass
        return self

    def open_chat_page(self) -> None:
        """Navigate to /chat (used by INBOX-006 to observe the sidebar)."""
        self._install_default_agent_init_script()
        self.page.goto(
            f"{config.base_url}/chat",
            wait_until="commit",
            timeout=self.timeout,
        )
        try:
            self.page.wait_for_load_state(
                "networkidle", timeout=self.timeout,
            )
        except TimeoutError:
            pass
