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

Playwright test configuration - Simple version without python-dotenv.
This assumes environment variables are loaded by the Makefile.
"""

# Standard
import logging
import os
import re
from typing import Dict, Generator, Optional
import uuid

# Third-Party
from playwright.sync_api import APIRequestContext, BrowserContext, expect, Page, Playwright
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
import pytest

# First-Party
from mcpgateway.config import Settings
from mcpgateway.utils.create_jwt_token import _create_jwt_token

# Local
from .pages.admin_page import AdminPage
from .pages.agents_page import AgentsPage
from .pages.gateways_page import GatewaysPage
from .pages.login_page import LoginPage
from .pages.mcp_registry_page import MCPRegistryPage
from .pages.metrics_page import MetricsPage
from .pages.plugins_page import PluginsPage
from .pages.prompts_page import PromptsPage
from .pages.resources_page import ResourcesPage
from .pages.servers_page import ServersPage
from .pages.team_page import TeamPage
from .pages.tokens_page import TokensPage
from .pages.tools_page import ToolsPage
from .pages.users_page import UsersPage
from .pages.version_page import VersionPage

# Get configuration from environment
BASE_URL = os.getenv("TEST_BASE_URL", "http://localhost:8080")
API_TOKEN = os.getenv("MCP_AUTH", "")
DISABLE_JWT_FALLBACK = os.getenv("PLAYWRIGHT_DISABLE_JWT_FALLBACK", "").lower() in ("1", "true", "yes")
PLAYWRIGHT_VIDEO_SIZE = os.getenv("PLAYWRIGHT_VIDEO_SIZE", "1920x1080")
PLAYWRIGHT_VIEWPORT_SIZE = os.getenv("PLAYWRIGHT_VIEWPORT_SIZE", PLAYWRIGHT_VIDEO_SIZE)

# Email login credentials (admin user)
ADMIN_EMAIL = os.getenv("PLATFORM_ADMIN_EMAIL", "admin@example.com")
ADMIN_PASSWORD = os.getenv("PLATFORM_ADMIN_PASSWORD", "5S1Nd8z$Ivb6N%Lsj^okvVF6")
ADMIN_NEW_PASSWORD = os.getenv("PLATFORM_ADMIN_NEW_PASSWORD", "SV^cB9Qx3!em48fy$1VhjxkW")
ADMIN_ACTIVE_PASSWORD = [ADMIN_PASSWORD]

# Ensure UI/Admin are enabled for tests
os.environ["MCPGATEWAY_UI_ENABLED"] = "true"
os.environ["MCPGATEWAY_ADMIN_API_ENABLED"] = "true"


@pytest.fixture(scope="session")
def base_url() -> str:
    """Base URL for the application."""
    return BASE_URL


def _format_auth_header(token: str) -> Optional[str]:
    """Normalize auth header value for API requests."""
    if not token:
        return None
    if token.lower().startswith(("bearer ", "basic ")):
        return token
    return f"Bearer {token}"


def _parse_video_size(size: str) -> Optional[Dict[str, int]]:
    """Parse WIDTHxHEIGHT size from env string."""
    if not size:
        return None
    match = re.match(r"^\s*(\d+)x(\d+)\s*$", size)
    if not match:
        raise ValueError("PLAYWRIGHT_VIDEO_SIZE must be in the format WIDTHxHEIGHT (e.g., 1280x720).")
    return {"width": int(match.group(1)), "height": int(match.group(2))}


VIDEO_SIZE = _parse_video_size(PLAYWRIGHT_VIDEO_SIZE)
VIEWPORT_SIZE = _parse_video_size(PLAYWRIGHT_VIEWPORT_SIZE)
logger = logging.getLogger(__name__)


def _wait_for_admin_transition(page: Page, previous_url: Optional[str] = None) -> None:
    """Wait for admin-related navigation after login actions."""
    try:
        page.wait_for_load_state("domcontentloaded", timeout=10000)
    except PlaywrightTimeoutError:
        logger.warning("Admin transition: domcontentloaded timed out (url=%s)", page.url)
    if previous_url and page.url == previous_url:
        # URL hasn't changed yet; wait for navigation to complete
        try:
            page.wait_for_url(lambda url: url != previous_url, timeout=10000)
        except PlaywrightTimeoutError:
            logger.warning("Admin transition: URL unchanged after wait (still %s)", page.url)


def _goto_admin(page: Page, url: str) -> None:
    """Navigate to an admin URL with domcontentloaded wait strategy.

    The caller (_ensure_admin_logged_in) already waits for specific UI
    elements (e.g. [data-testid="servers-tab"]) and JS initialisation,
    so networkidle is redundant and stalls under SSE/setInterval traffic.
    """
    try:
        page.goto(url, wait_until="domcontentloaded", timeout=60000)
    except Exception as e:
        if "ERR_TOO_MANY_REDIRECTS" in str(e):
            raise AssertionError(f"Admin page redirect loop detected at {url}. Server may be in a bad state or auth is misconfigured.") from e
        raise


def _submit_login_and_wait(page: Page, login_page, email: str, password: str) -> Optional[int]:
    """Submit login form and return the POST response status code."""
    try:
        with page.expect_response(lambda resp: "/admin/login" in resp.url and resp.request.method == "POST", timeout=10000) as response_info:
            login_page.submit_login(email, password)
        return response_info.value.status
    except PlaywrightTimeoutError:
        return None


def _candidate_admin_passwords(settings_obj: Settings, current_password: Optional[str] = None) -> list[str]:
    """Build ordered unique admin password candidates for resilient login retries."""
    configured_password = settings_obj.platform_admin_password.get_secret_value()
    candidates: list[str] = []
    for value in [current_password, ADMIN_ACTIVE_PASSWORD[0], ADMIN_NEW_PASSWORD, ADMIN_PASSWORD, configured_password]:
        if value and value not in candidates:
            candidates.append(value)
    return candidates


def _attempt_admin_login_with_password(page: Page, login_page, email: str, password: str) -> bool:
    """Attempt login using one password and handle password-change flow.

    Returns True when the browser is no longer on /admin/login after the attempt.
    """
    status = _submit_login_and_wait(page, login_page, email, password)
    if status is not None and status >= 500:
        raise AssertionError(f"Login failed with status {status}")

    _wait_for_admin_transition(page)

    password_rotated = False
    if login_page.is_on_change_password_page():
        login_page.submit_password_change(password, ADMIN_NEW_PASSWORD)
        ADMIN_ACTIVE_PASSWORD[0] = ADMIN_NEW_PASSWORD
        password_rotated = True
        _wait_for_admin_transition(page)

    if not login_page.is_on_login_page():
        if not password_rotated:
            ADMIN_ACTIVE_PASSWORD[0] = password
        return True
    return False


def _retry_ui_login_before_jwt(page: Page, login_page, admin_email: str, settings_obj: Settings, current_password: Optional[str] = None) -> bool:
    """Retry login with alternate password candidates before JWT cookie fallback."""
    for candidate in _candidate_admin_passwords(settings_obj, current_password):
        if current_password and candidate == current_password:
            continue
        if _attempt_admin_login_with_password(page, login_page, admin_email, candidate):
            return True
    return False


def _set_admin_jwt_cookie(page: Page, email: str) -> None:
    """Seed an admin JWT cookie to bypass UI login when credentials are unknown.

    Uses cookie-only auth (no Authorization header) to avoid CORS preflight
    failures on cross-origin CDN requests that carry crossorigin="anonymous"
    (required by SRI integrity attributes).
    """
    try:
        token = _create_jwt_token({"sub": email}, user_data={"email": email, "is_admin": True, "auth_provider": "local"}, teams=None)
    except Exception as exc:  # pragma: no cover - should only fail on misconfig
        raise AssertionError(f"Failed to create admin JWT token: {exc}") from exc

    cookie_url = f"{BASE_URL.rstrip('/')}/"
    page.context.add_cookies(
        [
            {
                "name": "jwt_token",
                "value": token,
                "url": cookie_url,
                "httpOnly": True,
                "sameSite": "Lax",
            }
        ]
    )


def _ensure_admin_logged_in(page: Page, base_url: str) -> None:
    """Ensure the page is logged into the admin interface.

    Prefers direct JWT cookie injection for resilience against shared password
    state and multi-worker routing.  Falls back to form-based login only when
    JWT injection is explicitly disabled via PLAYWRIGHT_DISABLE_JWT_FALLBACK.
    """
    settings = Settings()
    admin_email = settings.platform_admin_email or ADMIN_EMAIL

    # Create LoginPage instance (needed for fallback and recovery paths)
    login_page = LoginPage(page, base_url)
    current_password: Optional[str] = None

    if not DISABLE_JWT_FALLBACK:
        # ---- Primary path: inject a fresh JWT cookie per fixture ----
        _set_admin_jwt_cookie(page, admin_email)
        _goto_admin(page, "/admin/")
        _wait_for_admin_transition(page)
    else:
        # ---- Fallback: interactive form login (JWT disabled) ----
        _goto_admin(page, "/admin")
        landing_url = page.url
        if re.search(r"/admin/?(?:[?#].*)?$", landing_url):
            _wait_for_admin_transition(page, previous_url=landing_url)

        # Handle password change requirement
        if login_page.is_on_change_password_page():
            current_password = ADMIN_ACTIVE_PASSWORD[0] or settings.platform_admin_password.get_secret_value()
            login_page.submit_password_change(current_password, ADMIN_NEW_PASSWORD)
            ADMIN_ACTIVE_PASSWORD[0] = ADMIN_NEW_PASSWORD
            _wait_for_admin_transition(page)

        # Handle login page redirect if auth is required
        if login_page.is_on_login_page() or login_page.is_login_form_available():
            current_password = ADMIN_ACTIVE_PASSWORD[0] or settings.platform_admin_password.get_secret_value()
            _attempt_admin_login_with_password(page, login_page, admin_email, current_password)

            # Retry with known rotated password if credentials were invalid
            if login_page.has_invalid_credentials_error() and ADMIN_NEW_PASSWORD != current_password:
                _attempt_admin_login_with_password(page, login_page, admin_email, ADMIN_NEW_PASSWORD)

            # If login still failed, try remaining candidates
            if login_page.is_on_login_page():
                _retry_ui_login_before_jwt(page, login_page, admin_email, settings, current_password)
            if login_page.is_on_login_page():
                raise AssertionError("Admin login failed; set PLATFORM_ADMIN_PASSWORD or allow JWT fallback.")

    # Verify we're on the admin page (retry JWT injection up to 2 times for
    # intermittent cookie/redirect races).
    for _attempt in range(3):
        if "/admin/login" not in page.url:
            break
        if DISABLE_JWT_FALLBACK:
            break
        _set_admin_jwt_cookie(page, admin_email)
        _goto_admin(page, "/admin/")
        _wait_for_admin_transition(page)

    if "/admin/login" in page.url and not DISABLE_JWT_FALLBACK:
        # Rarely the JWT-cookie path races with the login redirect. Fall back
        # to the existing resilient form-login candidate flow instead of
        # failing the fixture outright.
        _goto_admin(page, "/admin")
        _wait_for_admin_transition(page)
        if login_page.is_on_login_page() or login_page.is_login_form_available():
            if not _retry_ui_login_before_jwt(page, login_page, admin_email, settings, current_password):
                raise AssertionError("Admin login failed after JWT fallback and form-login recovery.")

    expect(page).to_have_url(re.compile(r".*/admin(?!/login).*"), timeout=30000)

    # Wait for the application shell to load
    try:
        page.wait_for_selector('[data-testid="servers-tab"]', state="visible", timeout=60000)
    except PlaywrightTimeoutError:
        if "/admin/login" in page.url and not DISABLE_JWT_FALLBACK:
            # Recovery path for intermittent auth redirects during shell load.
            _set_admin_jwt_cookie(page, admin_email)
            _goto_admin(page, "/admin/")
            _wait_for_admin_transition(page)
            page.wait_for_selector('[data-testid="servers-tab"]', state="visible", timeout=30000)
            return

        content = page.content()
        if "Internal Server Error" in content:
            raise AssertionError("Admin page failed to load: Internal Server Error (500)")
        raise

    # Wait for JS initialization (showTab + HTMX + event delegation) before any tab clicks
    try:
        page.wait_for_function(
            "typeof window.Admin.showTab === 'function' && typeof window.htmx !== 'undefined' && window.Admin.eventDelegationInitialized === true",
            timeout=30000,
        )
    except PlaywrightTimeoutError:
        pass  # Continue — JS may be loaded via alternative path


@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
    """Create API request context with optional bearer token."""
    headers = {"Accept": "application/json"}

    token = API_TOKEN
    if not token and not DISABLE_JWT_FALLBACK:
        # Generate a fallback admin token for testing if none provided
        try:
            token = _create_jwt_token(
                {"sub": ADMIN_EMAIL},
                user_data={
                    "email": ADMIN_EMAIL,
                    "full_name": "Test Admin",
                    "is_admin": True,
                    "auth_provider": "test",
                },
                teams=None,  # Admin bypass: null teams with is_admin=true
            )
        except Exception:
            pass  # Use empty if generation fails

    auth_header = _format_auth_header(token)
    if auth_header:
        headers["Authorization"] = auth_header

    request_context = playwright.request.new_context(
        base_url=BASE_URL,
        extra_http_headers=headers,
    )
    yield request_context
    request_context.dispose()


@pytest.fixture(scope="session")
def browser_context_args(
    pytestconfig,
    playwright: Playwright,
    device: Optional[str],
    base_url: Optional[str],
    _pw_artifacts_folder,
) -> Dict:
    """Customize Playwright browser context for artifacts + video quality."""
    context_args: Dict = {}
    if device:
        context_args.update(playwright.devices[device])
    if base_url:
        context_args["base_url"] = base_url

    video_option = pytestconfig.getoption("--video")
    capture_video = video_option in ["on", "retain-on-failure"]
    if capture_video:
        context_args["record_video_dir"] = _pw_artifacts_folder.name
        if VIDEO_SIZE:
            context_args["record_video_size"] = VIDEO_SIZE

    if VIEWPORT_SIZE and not device:
        context_args["viewport"] = VIEWPORT_SIZE

    return context_args


@pytest.fixture
def context(new_context) -> BrowserContext:
    """Create a browser context using pytest-playwright hooks for artifacts."""
    return new_context(ignore_https_errors=True)


# Fixture if you need the default page fixture name
@pytest.fixture
def authenticated_page(page: Page) -> Page:
    """Alias for page fixture."""
    return page


@pytest.fixture
def admin_page(page: Page, base_url: str) -> AdminPage:
    """Provide a logged-in AdminPage instance for UI tests."""
    _ensure_admin_logged_in(page, base_url)
    return AdminPage(page, base_url)


@pytest.fixture
def team_page(page: Page, base_url: str) -> TeamPage:
    """Provide a logged-in TeamPage instance for team tests."""
    _ensure_admin_logged_in(page, base_url)
    return TeamPage(page)


@pytest.fixture
def tokens_page(page: Page, base_url: str) -> TokensPage:
    """Provide a logged-in TokensPage instance for token tests."""
    _ensure_admin_logged_in(page, base_url)
    return TokensPage(page)


@pytest.fixture
def metrics_page(page: Page, base_url: str) -> MetricsPage:
    """Provide a logged-in MetricsPage instance for metrics tests."""
    _ensure_admin_logged_in(page, base_url)
    return MetricsPage(page)


@pytest.fixture
def tools_page(page: Page, base_url: str) -> ToolsPage:
    """Provide a logged-in ToolsPage instance for tool tests."""
    _ensure_admin_logged_in(page, base_url)
    return ToolsPage(page)


@pytest.fixture
def resources_page(page: Page, base_url: str) -> ResourcesPage:
    """Provide a logged-in ResourcesPage instance for resource tests."""
    _ensure_admin_logged_in(page, base_url)
    return ResourcesPage(page)


@pytest.fixture
def plugins_page(page: Page, base_url: str) -> PluginsPage:
    """Provide a logged-in PluginsPage instance for plugin tests."""
    _ensure_admin_logged_in(page, base_url)
    return PluginsPage(page)


@pytest.fixture
def prompts_page(page: Page, base_url: str) -> PromptsPage:
    """Provide a logged-in PromptsPage instance for prompt tests."""
    _ensure_admin_logged_in(page, base_url)
    return PromptsPage(page)


@pytest.fixture
def agents_page(page: Page, base_url: str) -> AgentsPage:
    """Provide a logged-in AgentsPage instance for A2A agent tests."""
    _ensure_admin_logged_in(page, base_url)
    return AgentsPage(page)


@pytest.fixture
def gateways_page(page: Page, base_url: str) -> GatewaysPage:
    """Provide a logged-in GatewaysPage instance for gateway tests."""
    _ensure_admin_logged_in(page, base_url)
    return GatewaysPage(page)


@pytest.fixture
def servers_page(page: Page, base_url: str) -> ServersPage:
    """Provide a logged-in ServersPage instance for virtual server tests."""
    _ensure_admin_logged_in(page, base_url)
    return ServersPage(page)


@pytest.fixture
def users_page(page: Page, base_url: str) -> UsersPage:
    """Provide a logged-in UsersPage instance for user management tests."""
    _ensure_admin_logged_in(page, base_url)
    return UsersPage(page)


@pytest.fixture
def version_page(page: Page, base_url: str) -> VersionPage:
    """Provide a logged-in VersionPage instance for version info tests."""
    _ensure_admin_logged_in(page, base_url)
    return VersionPage(page)


@pytest.fixture
def mcp_registry_page(page: Page, base_url: str) -> MCPRegistryPage:
    """Provide a logged-in MCPRegistryPage instance for MCP Registry tests."""
    _ensure_admin_logged_in(page, base_url)
    return MCPRegistryPage(page)


@pytest.fixture
def test_tool_data():
    """Provide test data for tool creation."""
    unique_id = uuid.uuid4()
    return {
        "name": f"test-api-tool-{unique_id}",
        "description": "Test API tool for automation",
        "url": "https://httpbin.org/post",
        "integrationType": "REST",
        "requestType": "GET",
        "headers": '{"Authorization": "Bearer test-token"}',
        "input_schema": '{"type": "object", "properties": {"query": {"type": "string"}}}',
    }


@pytest.fixture
def test_server_data():
    """Provide test data for server creation."""
    unique_id = uuid.uuid4()
    return {
        "name": f"test-server-{unique_id}",
        "icon": "https://httpbin.org/icon.png",
    }


@pytest.fixture
def test_resource_data():
    """Provide test data for resource creation."""
    unique_id = uuid.uuid4()
    return {
        "uri": f"file:///tmp/test-resource-{unique_id}.txt",
        "name": f"Test Resource {unique_id}",
        "mimeType": "text/plain",
        "description": "A test resource created by automation",
    }


@pytest.fixture
def test_prompt_data():
    """Provide test data for prompt creation."""
    unique_id = uuid.uuid4()
    return {
        "name": f"test-prompt-{unique_id}",
        "description": "A test prompt created by automation",
        "arguments": '[{"name": "topic", "description": "Topic to discuss", "required": true}]',
    }


@pytest.fixture
def test_user_data():
    """Provide test data for user creation."""
    unique_id = uuid.uuid4().hex[:8]
    return {
        "email": f"test-user-{unique_id}@example.com",
        "full_name": f"Test User {unique_id}",
        "password": "TestP@ssw0rd!X9Secure2026",  # pragma: allowlist secret
    }


@pytest.fixture
def test_agent_data():
    """Provide test data for A2A agent creation."""
    unique_id = uuid.uuid4()
    return {
        "name": f"test-agent-{unique_id}",
        "endpoint_url": "https://httpbin.org/post",
        "agent_type": "generic",
        "description": "A test A2A agent created by automation",
        "tags": "test,automation,ai",
        "visibility": "public",
    }


# Pool of valid MCP server URLs for testing
# These are real, publicly available MCP servers that can be used for testing
VALID_MCP_SERVER_URLS = [
    "https://docs.mcp.cloudflare.com/sse",
    "https://mcp.deepwiki.com/sse",
    "https://mcp.openzeppelin.com/contracts/cairo/mcp",
    "https://mcp.openzeppelin.com/contracts/stylus/mcp",
    "https://mcp.openzeppelin.com/contracts/stellar/mcp",
    "https://mcp.openzeppelin.com/contracts/solidity/mcp",
]


@pytest.fixture
def test_gateway_data():
    """Provide test data for gateway creation."""
    unique_id = uuid.uuid4()
    # Use unique localhost URL to avoid conflicts and external service failures
    port = 49152 + (hash(str(unique_id)) % 16384)
    url = f"http://127.0.0.1:{port}"

    return {
        "name": f"test-gateway-{unique_id}",
        "url": url,
        "description": "A test MCP Server gateway created by automation",
        "tags": "test,automation,mcp",
        "transport": "SSE",
        "visibility": "public",
    }


@pytest.fixture
def test_gateway_with_basic_auth_data():
    """Provide test data for gateway with basic authentication."""
    unique_id = uuid.uuid4()
    # Use unique localhost URL to avoid conflicts and external service failures
    port = 49152 + (hash(str(unique_id)) % 16384)
    url = f"http://127.0.0.1:{port}"

    return {
        "name": f"test-gateway-basic-{unique_id}",
        "url": url,
        "description": "Test gateway with basic auth",
        "tags": "test,auth,basic",
        "transport": "SSE",
        "visibility": "public",
        "auth_type": "basic",
        "auth_username": "testuser",
        "auth_password": "TestP@ssw0rd!Test2026X",  # pragma: allowlist secret
    }


@pytest.fixture
def test_gateway_with_bearer_auth_data():
    """Provide test data for gateway with bearer token authentication."""
    unique_id = uuid.uuid4()
    # Use unique localhost URL to avoid conflicts and external service failures
    port = 49152 + (hash(str(unique_id)) % 16384)
    url = f"http://127.0.0.1:{port}"

    return {
        "name": f"test-gateway-bearer-{unique_id}",
        "url": url,
        "description": "Test gateway with bearer token auth",
        "tags": "test,auth,bearer",
        "transport": "SSE",
        "visibility": "private",
        "auth_type": "bearer",
        "auth_token": "test-bearer-token-12345",
    }


@pytest.fixture
def test_gateway_with_oauth_data():
    """Provide test data for gateway with OAuth 2.0 authentication."""
    unique_id = uuid.uuid4()
    # Use unique localhost URL to avoid conflicts and external service failures
    port = 49152 + (hash(str(unique_id)) % 16384)
    url = f"http://127.0.0.1:{port}"

    return {
        "name": f"test-gateway-oauth-{unique_id}",
        "url": url,
        "description": "Test gateway with OAuth 2.0",
        "tags": "test,auth,oauth",
        "transport": "SSE",
        "visibility": "team",
        "auth_type": "oauth",
        "oauth_grant_type": "client_credentials",
        "oauth_issuer": "http://localhost:3003",
        "oauth_token_url": "http://localhost:3003/token",
        "oauth_client_id": "test-client-id",
        "oauth_client_secret": "test-client-secret",
        "oauth_scopes": "openid profile email",
    }


@pytest.fixture(autouse=True)
def setup_test_environment(page: Page):
    """Set viewport and default timeout for consistent UI tests."""
    if VIEWPORT_SIZE:
        page.set_viewport_size(VIEWPORT_SIZE)
    page.set_default_timeout(60000)
    # Optionally, add request logging or interception here
