"""GUI tests for VS Code Web — login, extensions, LLM config, agent test.

Requires:
  - docker compose --profile vscode up -d  (VS Code on :8080, proxy on :5001)
  - playwright install chromium

Run:
  pytest tests/gui/test_gui_vscode.py -v --timeout=120 -m e2e
"""

from __future__ import annotations

import json
import os
import shutil
import time

import httpx
import pytest
from playwright.sync_api import Page, expect, sync_playwright

# ── Configuration ─────────────────────────────────────────────

VSCODE_URL = os.environ.get("VSCODE_URL", "http://localhost:8080")
VSCODE_PASSWORD = os.environ.get("VSCODE_PASSWORD", "proxym-vscode")
PROXY_URL = os.environ.get("PROXY_URL", "http://localhost:5001")
PROXY_API_KEY = os.environ.get("AI_PROXY_MASTER_KEY", "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")

# Extensions available from Open VSX (code-server marketplace)
REQUIRED_EXTENSIONS = [
    "continue.continue",
    "ms-python.python",
    "charliermarsh.ruff",
    "eamodio.gitlens",
    "mhutchie.git-graph",
    "humao.rest-client",
    "redhat.vscode-yaml",
    "tamasfe.even-better-toml",
    "mikestead.dotenv",
    "usernamehw.errorlens",
    "pkief.material-icon-theme",
]

# Extensions NOT in Open VSX — require manual .vsix install
VSIX_ONLY_EXTENSIONS = [
    "rooveterinary.roo-cline",
]

# Extensions that conflict with proxym (NOT available in code-server / Open VSX)
CONFLICTING_EXTENSIONS = [
    "github.copilot",
    "github.copilot-chat",
]

# VS Code project URL
VSCODE_PROJECT_URL = os.environ.get(
    "VSCODE_PROJECT_URL",
    "http://localhost:8080/?folder=/home/coder/project",
)

# ── Fixtures ──────────────────────────────────────────────────

def _e2e_enabled() -> bool:
    return os.environ.get("PROXYM_RUN_VSCODE_E2E", "0") == "1"


def _wait_for_service(url: str, timeout: int = 30) -> bool:
    """Poll URL until it responds (any status)."""
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            r = httpx.get(url, follow_redirects=True, timeout=5)
            if r.status_code < 500:
                return True
        except httpx.ConnectError:
            pass
        time.sleep(1)
    return False


@pytest.fixture(scope="module")
def vscode_ready():
    """Ensure VS Code Web is reachable before tests run."""
    if not _e2e_enabled():
        pytest.skip("Set PROXYM_RUN_VSCODE_E2E=1 to run VSCode E2E tests")
    if shutil.which("docker") is None:
        pytest.skip("docker not installed")
    try:
        r = httpx.get(VSCODE_URL, follow_redirects=True, timeout=3)
    except httpx.ConnectError:
        r = None
    if r is None or r.status_code >= 500:
        pytest.skip(
            f"VS Code Web not reachable at {VSCODE_URL}. "
            "Run: docker compose --profile vscode up -d"
        )


@pytest.fixture(scope="module")
def proxy_ready():
    """Ensure proxy API is reachable before tests run."""
    if not _e2e_enabled():
        pytest.skip("Set PROXYM_RUN_VSCODE_E2E=1 to run VSCode E2E tests")
    if shutil.which("docker") is None:
        pytest.skip("docker not installed")
    try:
        r = httpx.get(f"{PROXY_URL}/health", follow_redirects=True, timeout=3)
    except httpx.ConnectError:
        r = None
    if r is None or r.status_code >= 500:
        pytest.skip(
            f"Proxy not reachable at {PROXY_URL}. "
            "Run: docker compose up -d"
        )


@pytest.fixture(scope="module")
def browser_ctx():
    """Shared browser context for all tests in this module."""
    with sync_playwright() as p:
        try:
            browser = p.chromium.launch(headless=True)
        except Exception as e:
            pytest.skip(str(e))
        context = browser.new_context(
            viewport={"width": 1280, "height": 800},
            ignore_https_errors=True,
        )
        context.set_default_timeout(30_000)
        yield context
        context.close()
        browser.close()


@pytest.fixture(scope="module")
def logged_in_page(browser_ctx, vscode_ready):
    """Return a Page that is already logged in to code-server."""
    page = browser_ctx.new_page()
    page.goto(f"{VSCODE_URL}/login")
    page.wait_for_load_state("domcontentloaded")

    # Fill password and submit
    pw_input = page.locator("input[type='password']")
    pw_input.fill(VSCODE_PASSWORD)
    page.locator("button[type='submit'], input[type='submit']").first.click()

    # Wait for VS Code workbench to load (the main editor container)
    page.wait_for_selector(".monaco-workbench, #workbench\\.parts\\.editor", timeout=60_000)
    yield page
    page.close()


# ── 1. Login tests ────────────────────────────────────────────

class TestVSCodeLogin:
    """Test VS Code Web login flow."""

    def test_login_page_loads(self, browser_ctx, vscode_ready):
        """Login page should load with password form."""
        page = browser_ctx.new_page()
        try:
            page.goto(f"{VSCODE_URL}/login")
            page.wait_for_load_state("domcontentloaded")

            # Should have a password input
            pw_input = page.locator("input[type='password']")
            assert pw_input.count() >= 1, "No password input found on login page"

            # Should have a submit button
            submit = page.locator("button[type='submit'], input[type='submit']")
            assert submit.count() >= 1, "No submit button on login page"
        finally:
            page.close()

    def test_login_page_shows_branding(self, browser_ctx, vscode_ready):
        """Login page should show either proxym custom branding or default code-server."""
        page = browser_ctx.new_page()
        try:
            page.goto(f"{VSCODE_URL}/login")
            page.wait_for_load_state("domcontentloaded")
            content = page.content().lower()

            # Either our custom login page with proxym branding,
            # or the default code-server login page
            has_proxym = "proxym" in content
            has_code_server = "code-server" in content or "welcome" in content
            assert has_proxym or has_code_server, (
                "Login page has neither proxym branding nor code-server default"
            )
        finally:
            page.close()

    def test_wrong_password_rejected(self, browser_ctx, vscode_ready):
        """Wrong password should not grant access."""
        page = browser_ctx.new_page()
        try:
            page.goto(f"{VSCODE_URL}/login")
            page.wait_for_load_state("domcontentloaded")

            pw_input = page.locator("input[type='password']")
            pw_input.fill("wrong-password-12345")
            page.locator("button[type='submit'], input[type='submit']").first.click()

            page.wait_for_load_state("domcontentloaded")
            # Should still be on login page (not redirected to editor)
            assert "/login" in page.url or page.locator("input[type='password']").count() > 0, \
                "Wrong password should keep user on login page"
        finally:
            page.close()

    def test_correct_password_opens_editor(self, logged_in_page):
        """Correct password should open the VS Code editor."""
        page = logged_in_page
        # Should NOT be on login page anymore
        assert "/login" not in page.url, f"Still on login page: {page.url}"

        # Should see the workbench
        workbench = page.locator(".monaco-workbench")
        assert workbench.count() >= 1, "VS Code workbench not found after login"


# ── 2. Extensions verification ────────────────────────────────

class TestVSCodeExtensions:
    """Verify all required extensions are installed."""

    def test_extensions_installed_via_docker(self, vscode_ready):
        """Check extensions via docker exec on the container."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "code-server", "--list-extensions"],
            capture_output=True, text=True, timeout=15,
        )
        assert result.returncode == 0, f"Failed to list extensions: {result.stderr}"

        installed = result.stdout.strip().lower().splitlines()

        missing = []
        for ext in REQUIRED_EXTENSIONS:
            if ext.lower() not in installed:
                missing.append(ext)

        assert not missing, (
            f"Missing extensions: {missing}\n"
            f"Installed: {installed}"
        )

    def test_roo_code_extension_status(self, vscode_ready):
        """Report Roo Code installation status (not in Open VSX, needs .vsix)."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "code-server", "--list-extensions"],
            capture_output=True, text=True, timeout=15,
        )
        installed = result.stdout.strip().lower().splitlines()
        if "rooveterinary.roo-cline" not in installed:
            pytest.skip(
                "Roo Code (rooveterinary.roo-cline) not installed — "
                "not available in Open VSX marketplace. "
                "Install manually: code-server --install-extension roo-cline.vsix"
            )


# ── 3. LLM configuration verification ────────────────────────

class TestLLMConfig:
    """Verify VS Code settings have correct LLM/proxy configuration."""

    def test_settings_json_has_roo_code_config(self, vscode_ready):
        """settings.json should have Roo Code API configuration."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        assert result.returncode == 0, f"Cannot read settings.json: {result.stderr}"

        settings = json.loads(result.stdout)

        # Core Roo Code settings
        assert settings.get("roo-cline.apiProvider") == "openai", \
            f"Wrong apiProvider: {settings.get('roo-cline.apiProvider')}"
        assert "proxym" in settings.get("roo-cline.openAiBaseUrl", "").lower() or \
               "/v1" in settings.get("roo-cline.openAiBaseUrl", ""), \
            f"openAiBaseUrl not pointing to proxym: {settings.get('roo-cline.openAiBaseUrl')}"
        assert settings.get("roo-cline.openAiApiKey"), \
            "openAiApiKey is empty"

    def test_settings_has_mode_configs(self, vscode_ready):
        """settings.json should have per-mode API configs (code, architect, debug, ask)."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(result.stdout)

        mode_configs = settings.get("roo-cline.modeApiConfigs", {})
        required_modes = ["code", "architect", "debug", "ask"]
        for mode in required_modes:
            assert mode in mode_configs, f"Missing mode config: {mode}"
            cfg = mode_configs[mode]
            assert cfg.get("apiProvider") == "openai", \
                f"Mode '{mode}' has wrong apiProvider"
            assert cfg.get("openAiBaseUrl"), \
                f"Mode '{mode}' has no openAiBaseUrl"
            assert cfg.get("openAiApiKey"), \
                f"Mode '{mode}' has no openAiApiKey"
            assert cfg.get("openAiModelId"), \
                f"Mode '{mode}' has no openAiModelId"

    def test_settings_has_mcp_server(self, vscode_ready):
        """settings.json should configure the proxym MCP server."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(result.stdout)

        mcp_servers = settings.get("roo-cline.mcpServers", {})
        assert "proxym" in mcp_servers, "proxym MCP server not configured"
        assert "url" in mcp_servers["proxym"], "MCP server has no url"

    def test_settings_has_trust_disabled(self, vscode_ready):
        """Workspace trust should be disabled for seamless agent operation."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(result.stdout)

        assert settings.get("security.workspace.trust.enabled") is False, \
            "Workspace trust should be disabled"

    def test_settings_has_dark_theme(self, vscode_ready):
        """Color theme should be Dark+."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(result.stdout)

        assert settings.get("workbench.colorTheme") == "Dark+", \
            f"Wrong theme: {settings.get('workbench.colorTheme')}"

    def test_continue_config_exists(self, vscode_ready):
        """Continue.dev config should exist with proxym backends."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.continue/config.json"],
            capture_output=True, text=True, timeout=10,
        )
        if result.returncode != 0:
            pytest.skip("Continue config not generated yet (first start)")

        config = json.loads(result.stdout)
        models = config.get("models", [])
        assert len(models) >= 1, "Continue config has no models"
        assert any("proxym" in m.get("apiBase", "").lower() or
                    "/v1" in m.get("apiBase", "")
                    for m in models), \
            "Continue models not pointing to proxym"


# ── 4. Proxy LLM integration test ────────────────────────────

class TestProxyLLM:
    """Test that the proxy actually routes LLM requests."""

    def test_proxy_health(self, proxy_ready):
        """Proxy /health endpoint should be OK."""
        r = httpx.get(f"{PROXY_URL}/health", timeout=10)
        assert r.status_code == 200
        data = r.json()
        assert data["status"] == "ok"

    def test_proxy_models_available(self, proxy_ready):
        """Proxy should list available models."""
        r = httpx.get(
            f"{PROXY_URL}/v1/models",
            headers={"Authorization": f"Bearer {PROXY_API_KEY}"},
            timeout=10,
        )
        assert r.status_code == 200
        data = r.json()
        models = data.get("data", [])
        assert len(models) >= 1, "No models available from proxy"

        # Check that proxym aliases exist
        model_ids = [m["id"] for m in models]
        # At least one of the standard aliases should be present
        known_aliases = {"cheap", "balanced", "free", "premium", "local"}
        found_aliases = known_aliases & set(model_ids)
        assert found_aliases, (
            f"No proxym aliases found in models. Available: {model_ids}"
        )

    def test_proxy_chat_completion(self, proxy_ready):
        """Send a real chat completion through the proxy and get a response."""
        r = httpx.post(
            f"{PROXY_URL}/v1/chat/completions",
            headers={
                "Authorization": f"Bearer {PROXY_API_KEY}",
                "Content-Type": "application/json",
            },
            json={
                "model": "cheap",
                "messages": [
                    {"role": "user", "content": "Reply with exactly: PROXYM_TEST_OK"}
                ],
                "max_tokens": 50,
                "temperature": 0,
            },
            timeout=60,
        )
        assert r.status_code == 200, f"Chat completion failed: {r.status_code} {r.text}"
        data = r.json()
        assert "choices" in data, f"No choices in response: {data}"
        assert len(data["choices"]) >= 1
        content = data["choices"][0]["message"]["content"]
        assert content and len(content) > 0, "Empty response from LLM"

    def test_proxy_reachable_from_vscode_container(self, vscode_ready, proxy_ready):
        """VS Code container should be able to reach the proxy via internal network."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "curl", "-sf", "--max-time", "5",
             "http://proxy:4000/health"],
            capture_output=True, text=True, timeout=15,
        )
        assert result.returncode == 0, (
            f"VS Code container cannot reach proxy: {result.stderr}"
        )
        data = json.loads(result.stdout)
        assert data.get("status") == "ok"


# ── 5. Agent (Roo Code) smoke test ───────────────────────────

class TestRooCodeAgent:
    """Test that Roo Code agent can interact with the LLM via proxym."""

    def test_roo_code_sidebar_loads(self, logged_in_page, vscode_ready):
        """Roo Code sidebar panel should be accessible in VS Code."""
        import subprocess

        # First check if Roo Code is even installed
        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "code-server", "--list-extensions"],
            capture_output=True, text=True, timeout=15,
        )
        installed = result.stdout.strip().lower().splitlines()
        if "rooveterinary.roo-cline" not in installed:
            pytest.skip("Roo Code not installed (not in Open VSX)")

        page = logged_in_page

        # Try to open Roo Code sidebar via command palette
        page.keyboard.press("Control+Shift+P")
        page.wait_for_timeout(1000)

        command_input = page.locator(".quick-input-box input, .quickInput input").first
        if command_input.is_visible():
            command_input.fill("Roo Code")
            page.wait_for_timeout(1500)

            suggestions = page.locator(".quick-input-list .monaco-list-row, .quickInput .monaco-list-row")
            count = suggestions.count()

            page.keyboard.press("Escape")

            assert count >= 1, (
                "No Roo Code commands found in command palette. "
                "Extension may not be properly installed or activated."
            )
        else:
            page.keyboard.press("Escape")
            pytest.skip("Could not open command palette")

    def test_roo_code_api_connectivity(self, vscode_ready, proxy_ready):
        """Simulate Roo Code API call: OpenAI-compatible chat from inside VS Code container."""
        import subprocess

        # This simulates exactly what Roo Code does: calls the OpenAI API
        # through the proxym proxy using the configured base URL and key
        result = subprocess.run(
            ["docker", "exec", "proxym-vscode", "python3", "-c", """
import json, urllib.request, os, sys

url = os.environ.get("PROXYM_API_BASE", "http://proxy:4000/v1") + "/chat/completions"
key = os.environ.get("PROXYM_API_KEY", "")

payload = json.dumps({
    "model": "cheap",
    "messages": [{"role": "user", "content": "Say OK"}],
    "max_tokens": 10,
    "temperature": 0,
}).encode()

req = urllib.request.Request(url, data=payload, headers={
    "Authorization": f"Bearer {key}",
    "Content-Type": "application/json",
})

try:
    with urllib.request.urlopen(req, timeout=30) as resp:
        data = json.loads(resp.read())
        content = data["choices"][0]["message"]["content"]
        print(f"OK: {content[:50]}")
        sys.exit(0)
except Exception as e:
    print(f"FAIL: {e}", file=sys.stderr)
    sys.exit(1)
"""],
            capture_output=True, text=True, timeout=45,
        )
        assert result.returncode == 0, (
            f"Roo Code API connectivity failed from VS Code container:\n"
            f"stdout: {result.stdout}\n"
            f"stderr: {result.stderr}"
        )
        assert result.stdout.startswith("OK:"), f"Unexpected output: {result.stdout}"

    def test_roo_code_keybindings_configured(self, vscode_ready):
        """Roo Code keybindings should be configured."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/keybindings.json"],
            capture_output=True, text=True, timeout=10,
        )
        if result.returncode != 0:
            pytest.skip("keybindings.json not yet generated")

        keybindings = json.loads(result.stdout)
        commands = [kb.get("command", "") for kb in keybindings]

        assert "roo-cline.plusButtonClicked" in commands, \
            "Roo Code new chat keybinding (Ctrl+Shift+A) not configured"
        assert "roo-cline.popoutButtonClicked" in commands, \
            "Roo Code popout keybinding (Ctrl+Shift+R) not configured"

    def test_workspace_project_mounted(self, logged_in_page):
        """The workspace project directory should be mounted and visible."""
        page = logged_in_page

        # Open the Explorer sidebar
        page.keyboard.press("Control+Shift+E")
        page.wait_for_timeout(2000)

        # The explorer should show files from the mounted project
        explorer = page.locator(".explorer-folders-view, .explorer-viewlet, [id='workbench.view.explorer']")
        if explorer.count() > 0:
            # Should have some content visible
            explorer_text = explorer.first.inner_text()
            # The project root is mounted — there should be some files
            assert len(explorer_text.strip()) > 0, \
                "Explorer is empty — project may not be mounted"
        else:
            # Fallback: just check we can see the editor area
            assert page.locator(".monaco-workbench").count() >= 1


# ── 6. Copilot Chat conflict detection ───────────────────────

class TestCopilotChatConflict:
    """Verify GitHub Copilot Chat is NOT installed and won't cause errors.

    code-server uses Open VSX, not the MS marketplace. GitHub Copilot / Copilot Chat
    are proprietary MS extensions NOT available in Open VSX.
    Attempting to install them produces:
        'The extension GitHub.copilot-chat cannot be installed because it was not found.'
    """

    def test_copilot_extensions_not_installed(self, vscode_ready):
        """GitHub Copilot extensions must NOT be installed (not in Open VSX)."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "code-server", "--list-extensions"],
            capture_output=True, text=True, timeout=15,
        )
        assert result.returncode == 0, f"Failed: {result.stderr}"
        installed = [e.strip().lower() for e in result.stdout.strip().splitlines()]

        for ext in CONFLICTING_EXTENSIONS:
            assert ext.lower() not in installed, (
                f"CONFLICT: {ext} is installed! This extension is not available in "
                "code-server (Open VSX) and will cause errors: "
                "'The extension GitHub.copilot-chat cannot be installed because it was not found.' "
                "Remove it: code-server --uninstall-extension {ext}"
            )

    def test_copilot_not_in_settings(self, vscode_ready):
        """settings.json must NOT reference GitHub Copilot."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        assert result.returncode == 0
        settings_text = result.stdout.lower()

        assert "copilot" not in settings_text, (
            "settings.json contains 'copilot' references. "
            "GitHub Copilot is not available in code-server. "
            "Use Roo Code or Continue.dev as the AI assistant instead."
        )

    def test_copilot_install_would_fail(self, vscode_ready):
        """Attempting to install copilot-chat should fail (not in Open VSX)."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "code-server", "--install-extension", "github.copilot-chat",
             "--force"],
            capture_output=True, text=True, timeout=30,
        )
        # This SHOULD fail — copilot-chat is not in Open VSX
        if result.returncode == 0:
            # If it somehow succeeded, that's unexpected — uninstall it
            subprocess.run(
                ["docker", "exec", "proxym-vscode",
                 "code-server", "--uninstall-extension", "github.copilot-chat"],
                capture_output=True, text=True, timeout=15,
            )
            pytest.fail(
                "github.copilot-chat installed successfully — this should not happen "
                "in code-server with Open VSX marketplace"
            )
        # Expected: installation fails
        stderr = result.stderr.lower() + result.stdout.lower()
        assert "not found" in stderr or "error" in stderr or result.returncode != 0, (
            f"Unexpected copilot-chat install result: rc={result.returncode} "
            f"stdout={result.stdout} stderr={result.stderr}"
        )

    def test_alternative_ai_assistant_configured(self, vscode_ready):
        """Roo Code or Continue.dev should be configured as AI assistant (not Copilot)."""
        import subprocess

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(result.stdout)

        has_roo = settings.get("roo-cline.apiProvider") is not None
        has_continue = False
        # Check if Continue config exists
        cont_result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "test", "-f", "/home/coder/.continue/config.json"],
            capture_output=True, timeout=10,
        )
        has_continue = cont_result.returncode == 0

        assert has_roo or has_continue, (
            "No AI assistant configured! Neither Roo Code nor Continue.dev found. "
            "GitHub Copilot is NOT available in code-server. "
            "Configure roo-cline.apiProvider in settings.json."
        )


# ── 7. Chat setup error detection ────────────────────────────

class TestChatSetupErrors:
    """Test for common chat/agent setup errors.

    Covers:
      - 'An error occurred while setting up chat. Would you like to try again?'
      - Missing agent configuration
      - Extension marketplace unavailability
    """

    def test_roo_code_api_endpoint_reachable_from_container(self, vscode_ready, proxy_ready):
        """The Roo Code API endpoint (proxym) must be reachable from VS Code container.

        If unreachable, users see: 'An error occurred while setting up chat.'
        """
        import subprocess

        # Read the configured base URL from settings
        settings_result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(settings_result.stdout)
        base_url = settings.get("roo-cline.openAiBaseUrl", "")

        assert base_url, (
            "roo-cline.openAiBaseUrl is empty — agent cannot connect to LLM proxy. "
            "This causes: 'An error occurred while setting up chat.'"
        )

        # Test connectivity from inside the container
        # Replace host-based URL with internal docker network name
        test_url = base_url.rstrip("/")
        if not test_url.endswith("/models"):
            test_url += "/models"

        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "curl", "-sf", "--max-time", "10",
             "-H", f"Authorization: Bearer {settings.get('roo-cline.openAiApiKey', '')}",
             test_url],
            capture_output=True, text=True, timeout=20,
        )
        assert result.returncode == 0, (
            f"Cannot reach LLM API from VS Code container at {test_url}\n"
            f"stderr: {result.stderr}\n"
            "This causes: 'An error occurred while setting up chat. "
            "Would you like to try again?'"
        )

        data = json.loads(result.stdout)
        models = data.get("data", [])
        assert len(models) >= 1, (
            f"LLM API at {test_url} returned 0 models. "
            "Agent chat will fail without available models."
        )

    def test_api_key_is_valid(self, vscode_ready, proxy_ready):
        """The configured API key must authenticate successfully.

        Invalid key causes: 'An error occurred while setting up chat.'
        """
        import subprocess

        settings_result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(settings_result.stdout)
        api_key = settings.get("roo-cline.openAiApiKey", "")
        base_url = settings.get("roo-cline.openAiBaseUrl", "")

        assert api_key, (
            "roo-cline.openAiApiKey is empty — LLM requests will be rejected (401). "
            "Set PROXYM_API_KEY in .env"
        )

        # Verify the key works against the proxy
        r = httpx.get(
            f"{PROXY_URL}/v1/models",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=10,
        )
        assert r.status_code == 200, (
            f"API key rejected by proxy (HTTP {r.status_code}). "
            "This causes: 'An error occurred while setting up chat.'"
        )

    def test_agent_model_exists(self, vscode_ready, proxy_ready):
        """The model configured in Roo Code must exist in the proxy."""
        import subprocess

        settings_result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(settings_result.stdout)
        model_id = settings.get("roo-cline.openAiModelId", "")
        api_key = settings.get("roo-cline.openAiApiKey", "")

        assert model_id, (
            "roo-cline.openAiModelId is empty — agent has no model to use"
        )

        r = httpx.get(
            f"{PROXY_URL}/v1/models",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=10,
        )
        available = [m["id"] for m in r.json().get("data", [])]
        assert model_id in available, (
            f"Model '{model_id}' not found in proxy. Available: {available}. "
            "Agent will fail to generate responses."
        )

    def test_mode_models_all_exist(self, vscode_ready, proxy_ready):
        """All per-mode model IDs must be available in the proxy."""
        import subprocess

        settings_result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        settings = json.loads(settings_result.stdout)
        api_key = settings.get("roo-cline.openAiApiKey", "")
        mode_configs = settings.get("roo-cline.modeApiConfigs", {})

        r = httpx.get(
            f"{PROXY_URL}/v1/models",
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=10,
        )
        available = {m["id"] for m in r.json().get("data", [])}

        missing_models = []
        for mode, cfg in mode_configs.items():
            mid = cfg.get("openAiModelId", "")
            if mid and mid not in available:
                missing_models.append(f"{mode}: {mid}")

        assert not missing_models, (
            f"Mode model(s) not available in proxy: {missing_models}. "
            f"Available models: {sorted(available)}"
        )

    def test_extension_install_error_message(self, vscode_ready):
        """Verify the exact error message when installing unavailable extensions."""
        import subprocess

        # Try installing a known-unavailable extension to capture the error
        result = subprocess.run(
            ["docker", "exec", "proxym-vscode",
             "code-server", "--install-extension", "github.copilot-chat"],
            capture_output=True, text=True, timeout=30,
        )
        output = (result.stdout + result.stderr).lower()
        # Verify the error message matches what users report
        is_expected_error = (
            "not found" in output
            or "unable to install" in output
            or result.returncode != 0
        )
        assert is_expected_error, (
            "Expected extension install to fail with 'not found' error. "
            f"Got: rc={result.returncode} output={result.stdout + result.stderr}"
        )

    def test_project_folder_accessible(self, browser_ctx, vscode_ready):
        """VS Code should load with the project folder without errors."""
        page = browser_ctx.new_page()
        try:
            # Go directly to project URL (may redirect to login or load editor)
            page.goto(VSCODE_PROJECT_URL)
            page.wait_for_load_state("domcontentloaded")

            # Handle login if needed (fresh browser context)
            pw_input = page.locator("input[type='password']")
            if pw_input.count() > 0 and pw_input.is_visible():
                pw_input.fill(VSCODE_PASSWORD)
                page.locator("button[type='submit'], input[type='submit']").first.click()

            # Wait for workbench to load
            page.wait_for_selector(".monaco-workbench", timeout=60_000)

            # Should still have workbench (no crash)
            assert page.locator(".monaco-workbench").count() >= 1, \
                "VS Code crashed after opening project folder"

            # Wait a bit for any error notifications to appear
            page.wait_for_timeout(3000)

            # Check no critical error dialogs
            error_dialog = page.locator(".dialog-message, .notification-toast")
            if error_dialog.count() > 0:
                error_text = error_dialog.first.inner_text()
                # Copilot-chat error is a known issue we want to detect
                if "copilot" in error_text.lower():
                    pytest.fail(
                        f"Copilot error detected in VS Code: '{error_text}'. "
                        "Remove GitHub Copilot references from configuration."
                    )
        finally:
            page.close()


# ── 8. Diagnostics API verification ──────────────────────────

class TestDiagnosticsAPI:
    """Test the /tests/* backend diagnostic endpoints."""

    def test_diagnostics_status_endpoint(self, proxy_ready):
        """/tests/status should return comprehensive diagnostics."""
        r = httpx.get(
            f"{PROXY_URL}/tests/status",
            headers={"Authorization": f"Bearer {PROXY_API_KEY}"},
            timeout=30,
        )
        assert r.status_code == 200
        data = r.json()
        assert "overall_status" in data
        assert "tests" in data
        assert "passed" in data
        assert "total" in data
        assert data["total"] >= 8, f"Expected ≥8 tests, got {data['total']}"

    def test_diagnostics_vscode_check(self, proxy_ready, vscode_ready):
        """/tests/vscode should detect VS Code is running."""
        r = httpx.get(
            f"{PROXY_URL}/tests/vscode",
            headers={"Authorization": f"Bearer {PROXY_API_KEY}"},
            timeout=15,
        )
        assert r.status_code == 200
        data = r.json()
        assert data["service"] == "vscode"
        assert data["reachable"] is True, (
            f"Diagnostics says VS Code unreachable: {data.get('details')}"
        )

    def test_diagnostics_proxy_check(self, proxy_ready):
        """/tests/proxy should confirm proxy is healthy."""
        r = httpx.get(
            f"{PROXY_URL}/tests/proxy",
            headers={"Authorization": f"Bearer {PROXY_API_KEY}"},
            timeout=15,
        )
        assert r.status_code == 200
        data = r.json()
        assert data["service"] == "llm_proxy"
        assert data["reachable"] is True
        assert data["models_available"] >= 1

    def test_diagnostics_copilot_no_conflict(self, proxy_ready):
        """/tests/copilot should detect no Copilot conflicts."""
        r = httpx.get(
            f"{PROXY_URL}/tests/copilot",
            headers={"Authorization": f"Bearer {PROXY_API_KEY}"},
            timeout=15,
        )
        assert r.status_code == 200
        data = r.json()
        assert data["service"] == "copilot_conflict"
        # Should have no conflicts (copilot not installed)
        if not data.get("details", {}).get("skipped"):
            assert data["has_conflict"] is False, (
                f"Copilot conflict detected: {data.get('conflicting_extensions')}. "
                f"Recommendation: {data.get('recommendation')}"
            )

    def test_diagnostics_providers_configured(self, proxy_ready):
        """/tests/providers should show configured providers."""
        r = httpx.get(
            f"{PROXY_URL}/tests/providers",
            headers={"Authorization": f"Bearer {PROXY_API_KEY}"},
            timeout=15,
        )
        assert r.status_code == 200
        data = r.json()
        assert data["service"] == "providers"
        assert data["total_providers"] >= 1, "No LLM providers configured"

    def test_diagnostics_agent_setup(self, proxy_ready):
        """/tests/agent-setup should return readiness checks."""
        r = httpx.get(
            f"{PROXY_URL}/tests/agent-setup",
            headers={"Authorization": f"Bearer {PROXY_API_KEY}"},
            timeout=30,
        )
        assert r.status_code == 200
        data = r.json()
        assert data["service"] == "agent_setup"
        assert "checks" in data
        assert len(data["checks"]) >= 5, f"Expected ≥5 checks, got {len(data['checks'])}"

        # Log which checks passed/failed for debugging
        for check in data["checks"]:
            status = "✓" if check["pass"] else "✗"
            print(f"  {status} {check['name']}: {check['detail']}")
