"""Test the hypha server."""

import os
import subprocess
import sys
import asyncio
import os

import pytest
import requests
from hypha_rpc import connect_to_server

from . import (
    SERVER_URL,
    SERVER_URL_REDIS_1,
    SIO_PORT2,
    WS_SERVER_URL,
    find_item,
)

# All test coroutines will be treated as marked.
pytestmark = pytest.mark.asyncio


async def test_connect_to_server(fastapi_server):
    """Test connecting to the server."""

    class HyphaApp:
        """Represent a test app."""

        def __init__(self, ws):
            self._ws = ws

        async def setup(self):
            """Set up the app."""
            await self._ws.log("initialized")

        async def run(self, ctx):
            """Run the app."""
            await self._ws.log("hello world")

    # test workspace is an exception, so it can pass directly
    # rpc = await connect_to_server(
    #     {"name": "my app", "workspace": "public", "server_url": WS_SERVER_URL}
    # )
    with pytest.raises(
        Exception,
        match=r".*does not exist or is not accessible.*",
    ):
        rpc = await connect_to_server(
            {"name": "my app", "workspace": "test", "server_url": WS_SERVER_URL}
        )
    wm = await connect_to_server({"name": "my app", "server_url": WS_SERVER_URL})
    assert wm.config.workspace is not None
    assert wm.config.hypha_version
    assert wm.config.public_base_url.startswith("http")
    rpc = wm.rpc
    service_info = await rpc.register_service(HyphaApp(wm))
    service = await wm.get_service(service_info.id)
    assert await service.run(None) is None
    await wm.log("hello")


async def test_schema(fastapi_server):
    """Test schema."""
    api = await connect_to_server(
        {"name": "my app", "server_url": WS_SERVER_URL, "client_id": "my-app"}
    )
    for k in api:
        if callable(api[k]):
            assert (
                hasattr(api[k], "__schema__") and api[k].__schema__ is not None
            ), f"Schema not found for {k}"
            assert api[k].__schema__.get("name") == k, f"Schema name not match for {k}"


async def test_connect_to_server_two_client_same_id(fastapi_server):
    """Test connecting to the server with the same client id."""
    api1 = await connect_to_server(
        {"name": "my app", "server_url": WS_SERVER_URL, "client_id": "my-app"}
    )
    token = await api1.generate_token()
    try:
        api2 = await connect_to_server(
            {
                "name": "my app",
                "server_url": WS_SERVER_URL,
                "client_id": "my-app",
                "token": token,
                "workspace": api1.config.workspace,
            }
        )
        await api2.disconnect()
    except Exception as e:
        assert "Client already exists and is active:" in str(e)
    await api1.disconnect()


def test_plugin_runner(fastapi_server):
    """Test the app worker."""
    with subprocess.Popen(
        [
            sys.executable,
            "-m",
            "hypha.workers",
            f"--server-url={WS_SERVER_URL}",
            "--quit-on-ready",
            os.path.join(os.path.dirname(__file__), "example_plugin.py"),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    ) as proc:
        out, err = proc.communicate()
        assert err.decode("utf8") == ""
        output = out.decode("utf8")
        assert "This is a test" in output
        assert "echo: a message" in output


async def test_plugin_runner_subpath(fastapi_subpath_server):
    """Test the app worker with subpath server."""
    with subprocess.Popen(
        [
            sys.executable,
            "-m",
            "hypha.workers",
            f"--server-url=ws://127.0.0.1:{SIO_PORT2}/my/engine/ws",
            "--quit-on-ready",
            os.path.join(os.path.dirname(__file__), "example_plugin.py"),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    ) as proc:
        out, err = proc.communicate()
        assert err.decode("utf8") == ""
        output = out.decode("utf8")
        assert "This is a test" in output
        assert "echo: a message" in output


async def test_extra_mounts(fastapi_server):
    """Test mounting extra static files."""
    response = requests.get(f"{SERVER_URL}/tests/testWindowPlugin1.imjoy.html")
    assert response.ok
    assert "Test Window Plugin" in response.text


async def test_plugin_runner_workspace(fastapi_server):
    """Test the app worker with workspace."""
    api = await connect_to_server(
        {
            "name": "my second app",
            "server_url": WS_SERVER_URL,
        }
    )
    # Issue an admin token so the plugin can generate new token
    token = await api.generate_token()

    # The following code without passing the token should fail
    # Here we assert the output message contains "permission denied"
    with subprocess.Popen(
        [
            sys.executable,
            "-m",
            "hypha.workers",
            f"--server-url={WS_SERVER_URL}",
            f"--workspace={api.config['workspace']}",
            # f"--token={token}",
            "--quit-on-ready",
            os.path.join(os.path.dirname(__file__), "example_plugin.py"),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    ) as proc:
        out, err = proc.communicate()
        output = out.decode("utf8")
        assert proc.returncode == 1, err.decode("utf8")
        assert err.decode("utf8") == ""
        assert "Permission denied for " in output

    # now with the token, it should pass
    with subprocess.Popen(
        [
            sys.executable,
            "-m",
            "hypha.workers",
            f"--server-url={WS_SERVER_URL}",
            f"--workspace={api.config['workspace']}",
            f"--token={token}",
            "--quit-on-ready",
            os.path.join(os.path.dirname(__file__), "example_plugin.py"),
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    ) as proc:
        out, err = proc.communicate()
        output = out.decode("utf8")
        assert proc.returncode == 0, err.decode("utf8")
        assert err.decode("utf8") == ""
        assert "This is a test" in output
        assert "echo: a message" in output


async def test_services(fastapi_server):
    """Test services."""
    api = await connect_to_server({"name": "my app", "server_url": WS_SERVER_URL})

    token = await api.generate_token()
    assert token is not None

    service_info = await api.register_service(
        {
            "name": "test_service",
            "id": "test_service",
            "type": "test-type",
            "idx": 1,
        }
    )
    service = await api.get_service(service_info.id)
    assert service["name"] == "test_service"
    services = await api.list_services(api.config.workspace)
    assert find_item(services, "name", "test_service")

    service_info = await api.register_service(
        {
            "name": "test_service",
            "id": "test_service",
            "type": "test-type",
            "idx": 2,
        },
        {"overwrite": True},
    )
    # It should be overwritten because it's from the same provider
    assert (
        len(
            await api.list_services(
                {"workspace": api.config.workspace, "id": "test_service"}
            )
        )
        == 1
    )

    api2 = await connect_to_server(
        {
            "name": "my app 2",
            "server_url": WS_SERVER_URL,
            "token": token,
            "workspace": api.config.workspace,
        }
    )
    assert api2.config["workspace"] == api.config["workspace"]
    service_info = await api2.register_service(
        {
            "name": "test_service",
            "id": "test_service",
            "type": "test-type",
            "idx": 3,
        }
    )
    # It should be co-exist because it's from a different provider
    assert (
        len(
            await api.list_services(
                {"workspace": api.config.workspace, "id": "test_service"}
            )
        )
        == 2
    )
    assert (
        len(
            await api2.list_services(
                {"workspace": api2.config.workspace, "id": "test_service"}
            )
        )
        == 2
    )


async def test_workspace_owners(
    fastapi_server, test_user_token, test_user_token_temporary, test_user_token_2
):
    """Test workspace owners."""
    api = await connect_to_server(
        {
            "client_id": "my-app-99",
            "server_url": WS_SERVER_URL,
            "token": test_user_token,
        }
    )

    ws = await api.create_workspace(
        {
            "name": "my-test-workspace-owners",
            "description": "This is a test workspace",
            "owners": ["user-1@test.com", "user-2@test.com"],
        },
    )

    assert ws["name"] == "my-test-workspace-owners"

    api2 = await connect_to_server(
        {
            "server_url": WS_SERVER_URL,
            "token": test_user_token_2,
            "workspace": "my-test-workspace-owners",
        }
    )

    assert api2.config["workspace"] == "my-test-workspace-owners"

    try:
        api3 = await connect_to_server(
            {
                "server_url": WS_SERVER_URL,
                "token": test_user_token_temporary,
                "workspace": "my-test-workspace-owners",
            }
        )
        assert api3.config["workspace"] == "my-test-workspace-owners"
    except Exception as e:
        assert "Permission denied for workspace" in str(e)

    await api2.disconnect()
    await api.disconnect()


async def test_server_scalability(
    fastapi_server_redis_1, fastapi_server_redis_2, test_user_token
):
    """Test horizontal scalability with two Redis-backed Hypha servers.

    This test verifies that:
    1. Multiple Hypha instances can connect to the same Redis server
    2. Clients can connect to different server instances
    3. Services registered on one server are discoverable from another
    4. RPC calls work across server instances via Redis event bus

    The fastapi_server_redis_2 fixture uses a dynamically allocated port
    to avoid flaky 'address already in use' errors in CI.
    """
    # fastapi_server_redis_2 yields the server URL with dynamic port
    server_url_redis_2 = fastapi_server_redis_2

    api = await connect_to_server(
        {
            "client_id": "my-app-99",
            "server_url": server_url_redis_2,
            "token": test_user_token,
        }
    )

    ws = await api.create_workspace(
        {
            "name": "my-test-workspace",
            "description": "This is a test workspace",
            "owners": ["user1@imjoy.io", "user2@imjoy.io"],
        },
        overwrite=True,
    )
    assert ws["name"] == "my-test-workspace"

    token = await api.generate_token({"workspace": "my-test-workspace"})

    # Connect from two different servers
    api88 = await connect_to_server(
        {
            "client_id": "my-app-88",
            "server_url": SERVER_URL_REDIS_1,
            "workspace": "my-test-workspace",
            "token": token,
            "method_timeout": 30,
        }
    )

    api77 = await connect_to_server(
        {
            "client_id": "my-app-77",
            "server_url": server_url_redis_2,
            "workspace": "my-test-workspace",
            "token": token,
            "method_timeout": 30,
        }
    )
    await asyncio.sleep(0.2)

    clients = await api77.list_clients()
    assert find_item(clients, "id", "my-test-workspace/my-app-77")
    assert find_item(clients, "id", "my-test-workspace/my-app-88")

    await api77.register_service(
        {
            "id": "test-service",
            "add77": lambda x: x + 77,
        },
        {"overwrite": True},
    )

    svc = await api88.get_service("my-app-77:test-service")
    assert await svc.add77(1) == 78

    await api.disconnect()
    await api77.disconnect()
    await api88.disconnect()

async def test_root_token_authentication(fastapi_server):
    """Test root token authentication and admin-utils service access."""
    
    # Get the root token from environment (set by conftest.py)
    root_token = os.environ.get("HYPHA_ROOT_TOKEN")
    assert root_token, "Root token should be set by conftest.py"
    
    # Test 1: Connect with root token
    api_root = await connect_to_server(
        {
            "client_id": "root-client",
            "server_url": WS_SERVER_URL,
            "token": root_token,
        }
    )
    
    # Verify root user properties
    assert api_root.config["user"]["id"] == "root"
    assert "admin" in api_root.config["user"]["roles"]
    assert api_root.config["workspace"] == "ws-user-root"
    
    # Test 2: Access admin-utils service in root workspace
    # The admin-utils service should be available in the root workspace
    services = await api_root.list_services({"workspace": "ws-user-root"})
    # Find admin-utils service (it will have the server id prefix)
    admin_utils = None
    for svc in services:
        if "admin-utils" in svc["id"]:
            admin_utils = svc
            break
    assert admin_utils is not None, f"admin-utils service should exist in root workspace. Found services: {[s['id'] for s in services]}"
    
    # Test 3: Use admin-utils service (it should resolve correctly)
    admin_service = await api_root.get_service("admin-utils")
    
    # Test listing servers (admin capability)
    servers = await admin_service.list_servers()
    assert isinstance(servers, list)
    assert len(servers) > 0  # At least the current server should be listed
    
    # Test 4: List all workspaces using admin-utils
    all_workspaces = await admin_service.list_workspaces()
    assert len(all_workspaces) > 0
    # Should at least have root workspace and public workspace
    assert find_item(all_workspaces, "id", "ws-user-root")
    assert find_item(all_workspaces, "id", "public")
    
    # Test 5: Create a new workspace using root API
    test_ws = await api_root.create_workspace(
        {
            "name": "test-root-created-workspace",
            "description": "Workspace created by root user",
            "owners": ["root"],
        }
    )
    assert test_ws["name"] == "test-root-created-workspace"
    
    # Test 6: Generate token for the new workspace (admin capability)
    new_token = await api_root.generate_token({"workspace": test_ws["name"]})
    assert new_token is not None
    
    # Test 7: Connect with the generated token to verify it works
    api_test = await connect_to_server(
        {
            "client_id": "test-client",
            "server_url": WS_SERVER_URL,
            "token": new_token,
            "workspace": test_ws["name"],
        }
    )
    assert api_test.config["workspace"] == test_ws["name"]
    
    # Test 8: Get metrics (admin capability)
    metrics = await admin_service.get_metrics()
    assert isinstance(metrics, dict)
    
    # Test 9: Clean up - delete the test workspace
    await api_root.delete_workspace(test_ws["name"])
    
    # Verify workspace was deleted
    remaining_workspaces = await admin_service.list_workspaces()
    assert not find_item(remaining_workspaces, "name", "test-root-created-workspace")
    
    # Disconnect clients
    await api_test.disconnect()
    await api_root.disconnect()


async def test_root_token_rejection_weak_tokens(fastapi_server):
    """Test that weak root tokens are properly rejected."""
    
    # Test 1: Try to connect with a simple/weak token (should fail)
    weak_tokens = ["test", "root", "admin123", "password", "12345678901234567890123456789012"]
    
    for weak_token in weak_tokens:
        try:
            # This should fail because the server was started with a strong token
            # and these weak tokens won't match
            api = await connect_to_server(
                {
                    "client_id": "test-weak-token",
                    "server_url": WS_SERVER_URL,
                    "token": weak_token,
                }
            )
            await api.disconnect()
            # If we get here, the weak token was accepted (which shouldn't happen)
            assert False, f"Weak token '{weak_token}' should have been rejected"
        except Exception as e:
            # Expected behavior - weak token rejected
            assert "Permission denied" in str(e) or "Authentication error" in str(e)
    
    # Test 2: Try to connect without any token to a non-public workspace
    try:
        api = await connect_to_server(
            {
                "client_id": "no-token-client",
                "server_url": WS_SERVER_URL,
                "workspace": "ws-user-root",  # Root workspace requires authentication
            }
        )
        await api.disconnect()
        assert False, "Should not be able to access root workspace without token"
    except Exception as e:
        # Expected - permission denied
        assert "Permission denied" in str(e) or "does not exist" in str(e)


async def test_root_token_cross_workspace_operations(fastapi_server):
    """Test root token's ability to perform cross-workspace operations."""
    import os
    
    # Get the root token
    root_token = os.environ.get("HYPHA_ROOT_TOKEN")
    assert root_token, "Root token should be set"
    
    # Connect as root
    api_root = await connect_to_server(
        {
            "client_id": "root-cross-ws",
            "server_url": WS_SERVER_URL,
            "token": root_token,
        }
    )
    
    admin_service = await api_root.get_service("admin-utils")
    
    # Create two test workspaces using root API
    ws1 = await api_root.create_workspace(
        {
            "name": "test-ws-1",
            "description": "First test workspace",
            "owners": ["user1@example.com"],
        }
    )
    
    ws2 = await api_root.create_workspace(
        {
            "name": "test-ws-2", 
            "description": "Second test workspace",
            "owners": ["user2@example.com"],
        }
    )
    
    # Test cross-workspace service discovery (root should see all)
    ws1_services = await api_root.list_services({"workspace": ws1["name"]})
    ws2_services = await api_root.list_services({"workspace": ws2["name"]})
    
    # Root should be able to access both workspaces
    assert isinstance(ws1_services, list)
    assert isinstance(ws2_services, list)
    
    # Test creating services in different workspaces
    # Register a service in ws1
    token1 = await api_root.generate_token({"workspace": ws1["name"]})
    api_ws1 = await connect_to_server(
        {
            "client_id": "ws1-client",
            "server_url": WS_SERVER_URL,
            "token": token1,
            "workspace": ws1["name"],
        }
    )
    
    await api_ws1.register_service(
        {
            "id": "test-service-ws1",
            "name": "Test Service WS1",
            "type": "test",
            "echo": lambda x: f"ws1: {x}",
        }
    )
    
    # Register a service in ws2
    token2 = await api_root.generate_token({"workspace": ws2["name"]})
    api_ws2 = await connect_to_server(
        {
            "client_id": "ws2-client",
            "server_url": WS_SERVER_URL,
            "token": token2,
            "workspace": ws2["name"],
        }
    )
    
    await api_ws2.register_service(
        {
            "id": "test-service-ws2",
            "name": "Test Service WS2",
            "type": "test",
            "echo": lambda x: f"ws2: {x}",
        }
    )
    
    # Root should be able to see services in both workspaces
    all_ws1_services = await api_root.list_services({"workspace": ws1["name"]})
    all_ws2_services = await api_root.list_services({"workspace": ws2["name"]})
    
    # Check services exist (with full IDs)
    ws1_service_found = any("test-service-ws1" in s["id"] for s in all_ws1_services)
    ws2_service_found = any("test-service-ws2" in s["id"] for s in all_ws2_services)
    
    assert ws1_service_found, f"test-service-ws1 not found in {[s['id'] for s in all_ws1_services]}"
    assert ws2_service_found, f"test-service-ws2 not found in {[s['id'] for s in all_ws2_services]}"
    
    # Clean up
    await api_ws1.disconnect()
    await api_ws2.disconnect()
    
    # Delete workspaces (with error handling for potential race conditions)
    try:
        await api_root.delete_workspace(ws1["name"])
    except Exception:
        pass  # Workspace may already be deleted
    
    try:
        await api_root.delete_workspace(ws2["name"])
    except Exception:
        pass  # Workspace may already be deleted
    
    await api_root.disconnect()
