# -*- coding: utf-8 -*-
"""Location: ./tests/playwright/pages/servers_page.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

Servers page object for Virtual MCP Server management features.
"""

# Third-Party
from playwright.sync_api import expect, Locator
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError

# Local
from .base_page import BasePage


class ServersPage(BasePage):
    """Page object for Virtual MCP Server management features.

    This page manages the Virtual Servers (Catalog) tab where users can:
    - View and search virtual MCP servers
    - Create new virtual servers
    - Associate tools, resources, and prompts with servers
    - Configure OAuth settings
    - Manage server visibility and tags
    """

    # ==================== Panel Elements ====================

    @property
    def catalog_panel(self) -> Locator:
        """Catalog/Servers panel container."""
        return self.page.locator("#catalog-panel")

    @property
    def panel_title(self) -> Locator:
        """Panel title 'Virtual MCP Servers'."""
        return self.catalog_panel.locator("h2:has-text('Virtual MCP Servers')")

    # ==================== Search and Filter Elements ====================

    @property
    def search_input(self) -> Locator:
        """Server search input."""
        return self.page.locator('[data-testid="search-input"]')

    @property
    def clear_search_btn(self) -> Locator:
        """Clear search button."""
        return self.page.locator("#catalog-clear-search")

    @property
    def show_inactive_checkbox(self) -> Locator:
        """Show inactive servers checkbox."""
        return self.page.locator("#show-inactive-servers")

    # ==================== Server Table Elements ====================

    @property
    def servers_table(self) -> Locator:
        """Servers table."""
        return self.page.locator("#servers-table")

    @property
    def servers_table_body(self) -> Locator:
        """Servers table body."""
        return self.page.locator("#servers-table-body")

    @property
    def server_list(self) -> Locator:
        """Server list tbody element (alternative selector)."""
        return self.page.locator('[data-testid="server-list"]')

    @property
    def server_rows(self) -> Locator:
        """All server table rows."""
        return self.servers_table_body.locator("tr")

    @property
    def server_items(self) -> Locator:
        """All server items (alternative selector)."""
        return self.page.locator('[data-testid="server-item"]')

    # ==================== Server Form Elements ====================

    @property
    def add_server_form(self) -> Locator:
        """Add server form."""
        return self.page.locator("#add-server-form")

    @property
    def server_id_input(self) -> Locator:
        """Custom UUID input field (optional)."""
        return self.add_server_form.locator('[name="id"]')

    @property
    def server_name_input(self) -> Locator:
        """Server name input field."""
        return self.add_server_form.locator('[name="name"]')

    @property
    def server_description_input(self) -> Locator:
        """Server description textarea."""
        return self.add_server_form.locator('[name="description"]')

    @property
    def server_icon_input(self) -> Locator:
        """Server icon URL input field."""
        return self.add_server_form.locator('[name="icon"]')

    @property
    def server_tags_input(self) -> Locator:
        """Server tags input field."""
        return self.add_server_form.locator('[name="tags"]')

    @property
    def add_server_btn(self) -> Locator:
        """Add server submit button."""
        return self.add_server_form.get_by_role("button", name="Add Server")

    # ==================== Associated Items Elements ====================

    @property
    def associated_gateways_container(self) -> Locator:
        """Associated MCP Servers container."""
        return self.page.locator("#associatedGateways")

    @property
    def associated_tools_container(self) -> Locator:
        """Associated Tools container."""
        return self.page.locator("#associatedTools")

    @property
    def associated_resources_container(self) -> Locator:
        """Associated Resources container."""
        return self.page.locator("#associatedResources")

    @property
    def associated_prompts_container(self) -> Locator:
        """Associated Prompts container."""
        return self.page.locator("#associatedPrompts")

    @property
    def search_tools_input(self) -> Locator:
        """Search tools input."""
        return self.page.locator("#searchTools")

    @property
    def search_resources_input(self) -> Locator:
        """Search resources input."""
        return self.page.locator("#searchResources")

    @property
    def search_prompts_input(self) -> Locator:
        """Search prompts input."""
        return self.page.locator("#searchPrompts")

    @property
    def select_all_tools_btn(self) -> Locator:
        """Select all tools button."""
        return self.page.locator("#selectAllToolsBtn")

    @property
    def clear_all_tools_btn(self) -> Locator:
        """Clear all tools button."""
        return self.page.locator("#clearAllToolsBtn")

    # ==================== Visibility Radio Buttons ====================

    @property
    def visibility_public_radio(self) -> Locator:
        """Public visibility radio button."""
        return self.page.locator("#server-visibility-public")

    @property
    def visibility_team_radio(self) -> Locator:
        """Team visibility radio button."""
        return self.page.locator("#server-visibility-team")

    @property
    def visibility_private_radio(self) -> Locator:
        """Private visibility radio button."""
        return self.page.locator("#server-visibility-private")

    # ==================== OAuth Configuration Elements ====================

    @property
    def oauth_enabled_checkbox(self) -> Locator:
        """Enable OAuth 2.0 checkbox."""
        return self.page.locator("#server-oauth-enabled")

    @property
    def oauth_config_section(self) -> Locator:
        """OAuth configuration section."""
        return self.page.locator("#server-oauth-config-section")

    @property
    def oauth_authorization_server_input(self) -> Locator:
        """OAuth authorization server URL input."""
        return self.page.locator("#server-oauth-authorization-server")

    @property
    def oauth_scopes_input(self) -> Locator:
        """OAuth scopes input."""
        return self.page.locator("#server-oauth-scopes")

    @property
    def oauth_token_endpoint_input(self) -> Locator:
        """OAuth token endpoint URL input."""
        return self.page.locator("#server-oauth-token-endpoint")

    # ==================== High-Level Navigation Methods ====================

    def navigate_to_servers_tab(self) -> None:
        """Navigate to Servers/Catalog tab and wait for the table to finish loading."""
        self.sidebar.click_servers_tab()
        self.wait_for_servers_table_loaded()

    # ==================== High-Level Server Operations ====================

    def wait_for_servers_table_loaded(self, timeout: int = 30000) -> None:
        """Wait for servers table to be loaded and ready.

        Args:
            timeout: Maximum time to wait in milliseconds
        """
        self.page.wait_for_selector("#catalog-panel:not(.hidden)", timeout=timeout)
        # Wait for table body to exist in DOM (may be empty, so don't require visible)
        self.wait_for_attached(self.servers_table_body, timeout=timeout)

    def create_server(self, name: str, icon: str = "", description: str = "", tags: str = "", visibility: str = "public") -> None:
        """Create a new virtual server by filling and submitting the form.

        Args:
            name: Server name (required)
            icon: Server icon URL (optional)
            description: Server description (optional)
            tags: Comma-separated tags (optional)
            visibility: Visibility setting - "public", "team", or "private" (default: "public")
        """
        self.fill_locator(self.server_name_input, name)
        if icon:
            self.fill_locator(self.server_icon_input, icon)
        if description:
            self.fill_locator(self.server_description_input, description)
        if tags:
            self.fill_locator(self.server_tags_input, tags)

        # Set visibility
        if visibility == "team":
            self.click_locator(self.visibility_team_radio)
        elif visibility == "private":
            self.click_locator(self.visibility_private_radio)
        else:
            self.click_locator(self.visibility_public_radio)

        self.click_locator(self.add_server_btn)

    def fill_server_form(self, name: str, icon: str = "", description: str = "", tags: str = "", custom_id: str = "") -> None:
        """Fill the add server form with provided data (without submitting).

        Args:
            name: Server name (required)
            icon: Server icon URL (optional)
            description: Server description (optional)
            tags: Comma-separated tags (optional)
            custom_id: Custom UUID (optional)
        """
        if custom_id:
            self.fill_locator(self.server_id_input, custom_id)
        self.fill_locator(self.server_name_input, name)
        if icon:
            self.fill_locator(self.server_icon_input, icon)
        if description:
            self.fill_locator(self.server_description_input, description)
        if tags:
            self.fill_locator(self.server_tags_input, tags)

    def submit_server_form(self) -> None:
        """Submit the add server form."""
        self.click_locator(self.add_server_btn)

    def search_servers(self, query: str) -> None:
        """Search for servers using the search input.

        Search is server-side via HTMX with a debounce. Avoid Playwright
        ``networkidle`` because admin pages can keep long-lived requests open.
        Wait for table/indicator state instead.

        Args:
            query: Search query string
        """
        if query == "":
            self.clear_search()
            return

        self.fill_locator(self.search_input, query)

        try:
            self.page.wait_for_selector("#servers-loading.htmx-request", timeout=5000)
        except PlaywrightTimeoutError:
            # Fallback: explicitly trigger the server-side reload for the catalog panel.
            self.page.evaluate(
                "(q) => { const el = document.getElementById('servers-search-input'); if (el) { el.value = q; } if (window.loadSearchablePanel) { window.loadSearchablePanel('catalog'); } }",
                query,
            )
            try:
                self.page.wait_for_selector("#servers-loading.htmx-request", timeout=5000)
            except PlaywrightTimeoutError:
                pass

        self.page.wait_for_function(
            "() => !document.querySelector('#servers-loading.htmx-request')",
            timeout=15000,
        )
        try:
            self.page.wait_for_selector(
                "#servers-table-body",
                state="attached",
                timeout=15000,
            )
        except PlaywrightTimeoutError:
            # Recovery path: if the partial response did not restore the table
            # structure, hard-reload and reopen catalog.
            self.page.reload(wait_until="domcontentloaded")
            self.navigate_to_servers_tab()
            self.wait_for_servers_table_loaded()
            return

        # In some environments the clear action can leave the table in a stale
        # empty state (e.g., after a zero-result search). Recover by reloading
        # and re-opening the catalog tab so subsequent assertions see the
        # canonical server list.
        if self.get_server_count() == 0:
            self.page.reload(wait_until="domcontentloaded")
            self.navigate_to_servers_tab()
            self.wait_for_servers_table_loaded()

    def clear_search(self) -> None:
        """Clear the server search.

        Triggers an HTMX reload of the servers table, then waits for
        table/indicator settling.
        """
        request_seen = False
        try:
            with self.page.expect_response(
                lambda response: "/admin/servers/partial" in response.url and response.request.method == "GET",
                timeout=5000,
            ):
                self.click_locator(self.clear_search_btn)
            request_seen = True
        except PlaywrightTimeoutError:
            pass

        if not request_seen:
            # Fallback: invoke the same clear function the button uses and
            # explicitly wait for the partial reload request.
            try:
                with self.page.expect_response(
                    lambda response: "/admin/servers/partial" in response.url and response.request.method == "GET",
                    timeout=5000,
                ):
                    self.page.evaluate("window.Admin.clearSearch && window.Admin.clearSearch('catalog')")
                request_seen = True
            except PlaywrightTimeoutError:
                # Last-resort best effort: force a reload call even if request
                # observation missed due timing.
                self.page.evaluate(
                    "() => { const el = document.getElementById('servers-search-input'); if (el) { el.value = ''; } if (window.loadSearchablePanel) { window.loadSearchablePanel('catalog'); } }",
                )

        self.page.wait_for_function(
            "() => !document.querySelector('#servers-loading.htmx-request')",
            timeout=15000,
        )
        self.page.wait_for_selector("#servers-table-body", state="attached", timeout=15000)

    def toggle_show_inactive(self, show: bool = True) -> None:
        """Toggle the show inactive servers checkbox.

        Args:
            show: True to show inactive servers, False to hide them
        """
        self.page.wait_for_selector("#catalog-panel:not(.hidden)", timeout=15000)
        is_checked = self.show_inactive_checkbox.is_checked()
        if (show and not is_checked) or (not show and is_checked):
            self.click_locator(self.show_inactive_checkbox)

    def get_server_count(self) -> int:
        """Get number of servers displayed.

        Returns:
            Number of visible server rows
        """
        self.page.wait_for_selector("#catalog-panel:not(.hidden)", timeout=15000)
        self.page.wait_for_selector('[data-testid="server-list"]', state="attached")
        return self.server_items.locator(":visible").count()

    def server_exists(self, server_name: str) -> bool:
        """Check if a server with the given name exists in the table.

        Args:
            server_name: The name of the server to check

        Returns:
            True if server exists, False otherwise
        """
        return self.page.locator(f"text={server_name}").is_visible()

    def get_server_row(self, server_index: int) -> Locator:
        """Get a specific server row by index.

        Args:
            server_index: Index of the server row

        Returns:
            Locator for the server row
        """
        return self.server_rows.nth(server_index)

    # ==================== OAuth Configuration Methods ====================

    def enable_oauth(self, authorization_server: str, scopes: str = "openid profile email", token_endpoint: str = "") -> None:
        """Enable and configure OAuth 2.0 for the server.

        Args:
            authorization_server: OAuth authorization server URL (required)
            scopes: Space-separated OAuth scopes (default: "openid profile email")
            token_endpoint: OAuth token endpoint URL (optional)
        """
        # Enable OAuth
        if not self.oauth_enabled_checkbox.is_checked():
            self.click_locator(self.oauth_enabled_checkbox)

        # Wait for config section to be visible
        self.wait_for_visible(self.oauth_config_section)

        # Fill OAuth configuration
        self.fill_locator(self.oauth_authorization_server_input, authorization_server)
        self.fill_locator(self.oauth_scopes_input, scopes)
        if token_endpoint:
            self.fill_locator(self.oauth_token_endpoint_input, token_endpoint)

    def disable_oauth(self) -> None:
        """Disable OAuth 2.0 for the server."""
        if self.oauth_enabled_checkbox.is_checked():
            self.click_locator(self.oauth_enabled_checkbox)

    # ==================== Associated Items Methods ====================

    def select_tool(self, tool_name: str) -> None:
        """Select a tool to associate with the server.

        Args:
            tool_name: Name of the tool to select
        """
        tool_checkbox = self.associated_tools_container.locator(f'label:has-text("{tool_name}") input[type="checkbox"]')
        if not tool_checkbox.is_checked():
            self.click_locator(tool_checkbox)

    def select_all_tools(self) -> None:
        """Select all available tools."""
        self.click_locator(self.select_all_tools_btn)

    def clear_all_tools(self) -> None:
        """Clear all selected tools."""
        self.click_locator(self.clear_all_tools_btn)

    def wait_for_server_visible(self, server_name: str, timeout: int = 30000) -> None:
        """Wait for a server to be visible in the table.

        Args:
            server_name: The name of the server
            timeout: Maximum time to wait in milliseconds
        """
        self.page.wait_for_selector(f"text={server_name}", timeout=timeout)
        expect(self.page.locator(f"text={server_name}")).to_be_visible()

    def wait_for_server_hidden(self, server_name: str) -> None:
        """Wait for a server to be hidden from the table.

        Args:
            server_name: The name of the server
        """
        expect(self.page.locator(f"text={server_name}")).to_be_hidden()

    # ==================== Edit Server Modal Elements ====================

    @property
    def edit_server_modal(self) -> Locator:
        """Edit server modal container."""
        return self.page.locator("#server-edit-modal")

    @property
    def edit_server_name_input(self) -> Locator:
        """Name input inside the edit server modal."""
        return self.edit_server_modal.locator('input[name="name"]')

    @property
    def edit_server_cancel_btn(self) -> Locator:
        """Cancel button inside the edit server modal."""
        return self.edit_server_modal.locator('button:has-text("Cancel")')

    @property
    def edit_server_save_btn(self) -> Locator:
        """Save Changes button inside the edit server modal."""
        return self.edit_server_modal.locator('button:has-text("Save Changes")')

    @property
    def edit_tools_container(self) -> Locator:
        """Associated Tools container in edit modal."""
        return self.page.locator("#edit-server-tools")

    @property
    def edit_tools_search_input(self) -> Locator:
        """Tools search input in edit modal."""
        return self.page.locator("#searchEditTools")

    @property
    def edit_select_all_tools_btn(self) -> Locator:
        """Select All tools button in edit modal."""
        return self.page.locator("#selectAllEditToolsBtn")

    @property
    def edit_resources_container(self) -> Locator:
        """Associated Resources container in edit modal."""
        return self.page.locator("#edit-server-resources")

    @property
    def edit_resources_search_input(self) -> Locator:
        """Resources search input in edit modal."""
        return self.page.locator("#searchEditResources")

    @property
    def edit_select_all_resources_btn(self) -> Locator:
        """Select All resources button in edit modal."""
        return self.page.locator("#selectAllEditResourcesBtn")

    @property
    def edit_prompts_container(self) -> Locator:
        """Associated Prompts container in edit modal."""
        return self.page.locator("#edit-server-prompts")

    @property
    def edit_prompts_search_input(self) -> Locator:
        """Prompts search input in edit modal."""
        return self.page.locator("#searchEditPrompts")

    @property
    def edit_select_all_prompts_btn(self) -> Locator:
        """Select All prompts button in edit modal."""
        return self.page.locator("#selectAllEditPromptsBtn")

    # ==================== Edit Server Modal Methods ====================

    def open_edit_modal(self, server_name: str) -> None:
        """Find a server row by name and click its Edit button.

        Args:
            server_name: Name of the server to edit
        """
        server_row = self.page.locator(f'[data-testid="server-item"]:has-text("{server_name}")').first
        expect(server_row).to_be_visible(timeout=10000)
        self._open_action_dropdown(server_row)
        edit_btn = server_row.locator('button[role="menuitem"]:has-text("Edit")')
        edit_btn.click()
        expect(self.edit_server_modal).to_be_visible(timeout=10000)

    def get_edit_checked_tools(self) -> list[str]:
        """Return values of checked tool checkboxes in the edit modal."""
        return self.page.evaluate("""
            () => {
                const container = document.getElementById('edit-server-tools');
                if (!container) return [];
                return Array.from(
                    container.querySelectorAll('input[name="associatedTools"]:checked')
                ).map(cb => cb.value);
            }
        """)

    def get_edit_checked_resources(self) -> list[str]:
        """Return values of checked resource checkboxes in the edit modal."""
        return self.page.evaluate("""
            () => {
                const container = document.getElementById('edit-server-resources');
                if (!container) return [];
                return Array.from(
                    container.querySelectorAll('input[name="associatedResources"]:checked')
                ).map(cb => cb.value);
            }
        """)

    def get_edit_checked_prompts(self) -> list[str]:
        """Return values of checked prompt checkboxes in the edit modal."""
        return self.page.evaluate("""
            () => {
                const container = document.getElementById('edit-server-prompts');
                if (!container) return [];
                return Array.from(
                    container.querySelectorAll('input[name="associatedPrompts"]:checked')
                ).map(cb => cb.value);
            }
        """)

    def get_edit_tool_store_size(self) -> int:
        """Read the in-memory editServerSelections store size for tools."""
        return self._get_edit_store_size("edit-server-tools")

    def get_edit_resource_store_size(self) -> int:
        """Read the in-memory editServerSelections store size for resources."""
        return self._get_edit_store_size("edit-server-resources")

    def get_edit_prompt_store_size(self) -> int:
        """Read the in-memory editServerSelections store size for prompts."""
        return self._get_edit_store_size("edit-server-prompts")

    def _get_edit_store_size(self, key: str) -> int:
        """Read the in-memory editServerSelections store size for a given key."""
        return self.page.evaluate(
            """
            (key) => {
                const store = window.Admin.AppState && window.Admin.AppState.editServerSelections && window.Admin.AppState.editServerSelections[key];
                return store ? store.size : 0;
            }
        """,
            key,
        )
