"""Integration test for Astrolabe Plotly 3D visualization with multi-user BasicAuth mode.

Cross-system interface test: Tests the MCP server's integration with the
Astrolabe Nextcloud app, which is installed from the Nextcloud app store via
app-hooks/post-installation/20-install-astrolabe-app.sh. Astrolabe source
lives in a separate repository (https://github.com/cbcoutinho/astrolabe).

This test verifies that:
1. User can provision background sync access via app password
2. Content created via MCP tools is indexed by vector sync
3. Semantic search via Astrolabe UI returns results
4. Plotly 3D visualization container renders correctly

Requires:
- docker-compose up -d app db mcp-multi-user-basic
- ENABLE_SEMANTIC_SEARCH=true on the mcp-multi-user-basic container
"""

import base64
import json
import logging
import re
import uuid

import anyio
import pytest
from playwright.async_api import Page

# Import helper functions from existing test
from tests.conftest import create_mcp_client_session
from tests.integration.test_astrolabe_multi_user_background_sync import (
    complete_astrolabe_authorization,
    login_to_nextcloud,
)

logger = logging.getLogger(__name__)

pytestmark = [pytest.mark.integration, pytest.mark.multi_user_basic]


async def wait_for_vector_sync(
    mcp_client, initial_indexed_count: int, timeout_seconds: int = 60
) -> tuple[bool, dict | None]:
    """Wait for vector sync to index new content.

    Args:
        mcp_client: MCP client session
        initial_indexed_count: Initial indexed document count before creating content
        timeout_seconds: Maximum time to wait for sync

    Returns:
        Tuple of (success, status_data)
    """
    wait_interval = 2
    waited = 0
    status_data = None

    while waited < timeout_seconds:
        sync_status = await mcp_client.call_tool("nc_get_vector_sync_status", {})
        if sync_status.isError:
            logger.warning(f"Vector sync status error: {sync_status}")
            return False, None

        status_data = json.loads(sync_status.content[0].text)
        indexed_count = status_data.get("indexed_count", 0)
        pending_count = status_data.get("pending_count", 1)

        logger.info(
            f"Sync status at {waited}s: indexed={indexed_count}, "
            f"pending={pending_count}, status={status_data.get('status')}"
        )

        if indexed_count > initial_indexed_count and pending_count == 0:
            logger.info(
                f"✓ Sync complete: {indexed_count} documents indexed "
                f"(was {initial_indexed_count})"
            )
            return True, status_data

        await anyio.sleep(wait_interval)
        waited += wait_interval

    return False, status_data


async def navigate_to_astrolabe_main(page: Page):
    """Navigate to Astrolabe main app page (Semantic Search section).

    Args:
        page: Playwright page instance (must be authenticated)
    """
    nextcloud_url = "http://localhost:8080"

    logger.info("Navigating to Astrolabe main app...")
    await page.goto(f"{nextcloud_url}/apps/astrolabe", wait_until="networkidle")

    # Wait for the app to load
    await anyio.sleep(1)

    logger.info("✓ Successfully loaded Astrolabe main app")


@pytest.mark.integration
@pytest.mark.multi_user_basic
@pytest.mark.timeout(
    300
)  # 5 minutes - this test involves OAuth, app password, and vector sync
async def test_astrolabe_plotly_visualization_with_basic_auth(
    browser,
    test_users_setup,
    configure_astrolabe_for_mcp_server,
):
    """Test Plotly 3D visualization in Astrolabe with multi-user BasicAuth mode.

    This test:
    1. Configures Astrolabe for the mcp-multi-user-basic service
    2. Provisions background sync access for alice via app password
    3. Creates a note with unique searchable content (as alice)
    4. Waits for vector sync to index the note
    5. Performs semantic search in Astrolabe UI
    6. Verifies the Plotly visualization renders and results are displayed
    """
    # Phase 1: Configure Astrolabe for mcp-multi-user-basic
    await configure_astrolabe_for_mcp_server(
        mcp_server_internal_url="http://mcp-multi-user-basic:8000",
        mcp_server_public_url="http://localhost:8003",
    )

    username = "alice"
    password = test_users_setup[username]["password"]
    note_id = None
    unique_term = None

    # Create MCP client with alice's credentials for the multi-user BasicAuth server
    credentials = base64.b64encode(f"{username}:{password}".encode()).decode("utf-8")
    auth_header = f"Basic {credentials}"

    context = await browser.new_context(ignore_https_errors=True)
    page = await context.new_page()

    try:
        # Phase 2: Complete full Astrolabe authorization (OAuth + app password)
        await login_to_nextcloud(page, username, password)
        auth_result = await complete_astrolabe_authorization(page, username, password)
        logger.info(f"Authorization result: {auth_result}")

        # Create MCP client session as alice - all MCP operations inside this block
        async for alice_mcp_client in create_mcp_client_session(
            url="http://localhost:8003/mcp",
            headers={"Authorization": auth_header},
            client_name="Alice BasicAuth MCP",
        ):
            # Phase 3: Get initial indexed count
            initial_sync = await alice_mcp_client.call_tool(
                "nc_get_vector_sync_status", {}
            )

            if initial_sync.isError:
                pytest.skip("Vector sync not enabled on mcp-multi-user-basic")

            initial_data = json.loads(initial_sync.content[0].text)
            initial_count = initial_data.get("indexed_count", 0)
            logger.info(f"Initial indexed count: {initial_count}")

            # Create note with unique searchable term
            unique_term = f"plotly_viz_test_{uuid.uuid4().hex[:8]}"
            note_response = await alice_mcp_client.call_tool(
                "nc_notes_create_note",
                {
                    "title": f"Visualization Test Note {unique_term}",
                    "content": f"""# Testing Plotly Visualization

This note contains the unique term: {unique_term}

It is used to test the 3D vector space visualization in the Astrolabe app.
The visualization should show this document as a point in PCA-reduced space.

## Key Features
- Semantic search with embeddings
- PCA dimension reduction to 3D
- Interactive Plotly scatter3d plot
""",
                    "category": "Test",
                },
            )

            if note_response.isError:
                pytest.fail(f"Failed to create test note: {note_response}")

            note_data = json.loads(note_response.content[0].text)
            note_id = note_data.get("id")
            logger.info(f"Created test note ID: {note_id}")

            # Phase 4: Wait for vector indexing
            sync_complete, status = await wait_for_vector_sync(
                alice_mcp_client, initial_count, timeout_seconds=90
            )
            assert sync_complete, f"Vector sync did not complete in time: {status}"

            # Phase 5: Navigate to Astrolabe and perform search
            await navigate_to_astrolabe_main(page)

            # Fill search query - find the Astrolabe search input specifically
            # The NcTextField component wraps the input in a div with class mcp-search-input
            search_input = page.locator(".mcp-search-input input")
            await search_input.wait_for(timeout=10000, state="visible")
            await search_input.fill(unique_term)
            logger.info(f"Entered search query: {unique_term}")

            # Trigger search by pressing Enter on the input field
            # This is wired to performSearch via @keyup.enter in the Vue component
            await search_input.press("Enter")
            logger.info("Pressed Enter to trigger search")

            # Wait for loading to complete - watch for loading indicator to disappear
            loading_indicator = page.locator(".mcp-loading")
            try:
                # If loading indicator appears, wait for it to disappear
                if await loading_indicator.count() > 0:
                    await loading_indicator.wait_for(state="hidden", timeout=30000)
                    logger.info("Loading completed")
            except Exception:
                # Loading might be too fast to catch
                pass

            # Brief wait for UI to settle
            await anyio.sleep(1)

            # Take diagnostic screenshot
            await page.screenshot(path="/tmp/astrolabe_search_after_click.png")
            logger.info(
                "Took diagnostic screenshot: /tmp/astrolabe_search_after_click.png"
            )

            # Wait for search results using text-based detection
            # This is more reliable than class-based selectors
            # The UI shows "N results" when search completes successfully
            results_text_pattern = page.get_by_text(re.compile(r"\d+ results?"))
            no_results_text = page.get_by_text("No results found")
            error_note = page.locator(".mcp-error")

            # Wait for one of: results count, no results message, or error
            try:
                # Poll for results or error states (don't rely on Nextcloud core CSS classes)
                found_state = False
                for attempt in range(60):  # 60 attempts, 500ms each = 30s total
                    if await error_note.count() > 0:
                        error_text = await error_note.text_content()
                        logger.error(f"Search error: {error_text}")
                        pytest.fail(f"Search failed with error: {error_text}")

                    if await no_results_text.count() > 0:
                        logger.warning(
                            "No results found - vector sync may not have completed"
                        )
                        await page.screenshot(path="/tmp/astrolabe_no_results.png")
                        pytest.fail(
                            f"Search returned no results for '{unique_term}'. "
                            "Check if vector sync completed for alice's content."
                        )

                    if await results_text_pattern.count() > 0:
                        results_text = await results_text_pattern.first.text_content()
                        logger.info(f"Found results: {results_text}")
                        found_state = True
                        break

                    if attempt % 10 == 0:
                        logger.info(
                            f"Waiting for results... (attempt {attempt + 1}/60)"
                        )

                    await anyio.sleep(0.5)

                if not found_state:
                    await page.screenshot(path="/tmp/astrolabe_search_timeout.png")
                    page_content = await page.content()
                    logger.error(f"Search state not resolved. Page URL: {page.url}")
                    logger.error(f"Page content snippet: {page_content[:2000]}")
                    raise AssertionError("Search did not complete within timeout")

            except AssertionError:
                raise  # Re-raise AssertionError as-is
            except Exception as e:
                # Take another screenshot and get page content for debugging
                await page.screenshot(path="/tmp/astrolabe_search_timeout.png")
                page_content = await page.content()
                logger.error(f"Search state not resolved. Page URL: {page.url}")
                logger.error(f"Page content snippet: {page_content[:2000]}")
                raise AssertionError(f"Search did not complete: {e}")

            logger.info("Results loaded")

            # Phase 6: Verify visualization
            # Check Plotly container is visible
            viz_plot = page.locator("#viz-plot")
            await viz_plot.wait_for(timeout=15000, state="visible")
            logger.info("Plotly container is visible")

            # Verify Plotly has rendered content (SVG/canvas elements inside)
            has_viz_content = await page.evaluate(
                """
                () => {
                    const plot = document.getElementById('viz-plot');
                    if (!plot) return false;
                    // Plotly creates .plotly class, canvas, or svg elements
                    return plot.children.length > 0 ||
                           plot.querySelector('.plotly, canvas, svg, .main-svg') !== null;
                }
            """
            )
            assert has_viz_content, "Plotly visualization did not render any content"
            logger.info("✓ Plotly visualization rendered content")

            # Verify results are displayed
            result_items = page.locator(".mcp-result-item")
            result_count = await result_items.count()
            assert result_count > 0, "No search results displayed"
            logger.info(f"✓ Found {result_count} search result(s)")

            # Verify our note appears in results
            found_note = False
            for i in range(result_count):
                item = result_items.nth(i)
                title_elem = item.locator(".mcp-result-title")
                title_text = await title_elem.text_content()
                if title_text and unique_term in title_text:
                    found_note = True
                    logger.info(f"✓ Found test note in results: {title_text}")
                    break

            assert found_note, f"Created note with '{unique_term}' not found in results"

            # Optional: Take screenshot for verification
            await page.screenshot(path="/tmp/astrolabe_plotly_test_success.png")
            logger.info("✓ All Plotly visualization assertions passed")

            # Cleanup: delete the created note (inside the MCP client context)
            if note_id:
                try:
                    delete_response = await alice_mcp_client.call_tool(
                        "nc_notes_delete_note", {"note_id": note_id}
                    )
                    if not delete_response.isError:
                        logger.info(f"✓ Cleaned up test note {note_id}")
                        note_id = None  # Mark as cleaned
                    else:
                        logger.warning(
                            f"Failed to delete note {note_id}: {delete_response}"
                        )
                except Exception as e:
                    logger.warning(f"Cleanup failed for note {note_id}: {e}")

    finally:
        # Cleanup note if not already cleaned (create new client for cleanup)
        if note_id:
            try:
                async for cleanup_client in create_mcp_client_session(
                    url="http://localhost:8003/mcp",
                    headers={"Authorization": auth_header},
                    client_name="Cleanup MCP",
                ):
                    delete_response = await cleanup_client.call_tool(
                        "nc_notes_delete_note", {"note_id": note_id}
                    )
                    if not delete_response.isError:
                        logger.info(f"✓ Cleaned up test note {note_id} (finally)")
                    else:
                        logger.warning(
                            f"Failed to delete note {note_id}: {delete_response}"
                        )
            except Exception as e:
                logger.warning(f"Cleanup failed for note {note_id}: {e}")

        # Close browser context
        await context.close()
