"""Group-level permission integration tests for MLflow OIDC Auth Plugin.

Test IDs: PERM-G-001 through PERM-G-013

Tests verify:
- Group membership grants correct permissions
- Users inherit permissions from their groups
- Default permissions apply when no explicit group permission exists
"""

from __future__ import annotations

import time
import uuid
from urllib.parse import quote

import httpx
import pytest

# =============================================================================
# Helper Functions
# =============================================================================


def _create_experiment(client: httpx.Client, experiment_name: str) -> tuple[bool, str]:
    """Create an experiment and return (success, experiment_id)."""
    get_api = f"ajax-api/2.0/mlflow/experiments/get-by-name?experiment_name={quote(experiment_name)}"
    create_api = "ajax-api/2.0/mlflow/experiments/create"

    get_resp = client.get(get_api)
    if get_resp.status_code == 200:
        payload = get_resp.json()
        exp = payload.get("experiment", payload)
        exp_id = exp.get("experiment_id") or exp.get("experimentId") or exp.get("id")
        return True, str(exp_id)

    create_resp = client.post(create_api, json={"name": experiment_name})
    if create_resp.status_code != 200:
        return False, f"Create failed: {create_resp.status_code} {create_resp.text}"

    get_resp2 = client.get(get_api)
    if get_resp2.status_code == 200:
        payload2 = get_resp2.json()
        exp2 = payload2.get("experiment", payload2)
        exp_id2 = exp2.get("experiment_id") or exp2.get("experimentId") or exp2.get("id")
        return True, str(exp_id2)

    return False, "Failed to get experiment after creation"


def _create_model(client: httpx.Client, model_name: str) -> bool:
    """Create a registered model."""
    get_api = f"ajax-api/2.0/mlflow/registered-models/get?name={quote(model_name)}"
    create_api = "ajax-api/2.0/mlflow/registered-models/create"

    get_resp = client.get(get_api)
    if get_resp.status_code == 200:
        return True

    create_resp = client.post(create_api, json={"name": model_name})
    return create_resp.status_code == 200


def _create_prompt(client: httpx.Client, prompt_name: str, prompt_text: str) -> bool:
    """Create a prompt."""
    get_api = f"ajax-api/2.0/mlflow/registered-models/get?name={quote(prompt_name)}"
    create_api = "ajax-api/2.0/mlflow/registered-models/create"
    version_api = "ajax-api/2.0/mlflow/model-versions/create"

    get_resp = client.get(get_api)
    if get_resp.status_code == 200:
        return True

    create_resp = client.post(
        create_api,
        json={
            "name": prompt_name,
            "tags": [{"key": "mlflow.prompt.is_prompt", "value": "true"}],
        },
    )
    if create_resp.status_code != 200:
        return False

    version_resp = client.post(
        version_api,
        json={
            "name": prompt_name,
            "description": "Initial version",
            "source": "test-source",
            "tags": [
                {"key": "mlflow.prompt.is_prompt", "value": "true"},
                {"key": "mlflow.prompt.text", "value": prompt_text},
            ],
        },
    )
    return version_resp.status_code == 200


def _set_group_experiment_permission(
    client: httpx.Client,
    experiment_id: str,
    group_name: str,
    permission: str,
    base_url: str,
) -> bool:
    """Set group-level experiment permission using RESTful API."""
    # New RESTful API path
    api_url = f"api/2.0/mlflow/permissions/groups/{quote(group_name)}/experiments/{quote(experiment_id)}"

    # Delete existing permission first (ignore errors - permission may not exist)
    # Note: Server returns 500 when permission doesn't exist, not 404
    client.request("DELETE", api_url)

    # Create new permission with POST
    resp = client.post(api_url, json={"permission": permission})

    return resp.status_code in (200, 201)


def _set_group_model_permission(
    client: httpx.Client,
    model_name: str,
    group_name: str,
    permission: str,
) -> bool:
    """Set group-level model permission using RESTful API."""
    # New RESTful API path
    api_url = f"api/2.0/mlflow/permissions/groups/{quote(group_name)}/registered-models/{quote(model_name)}"

    # Delete existing permission first (ignore errors - permission may not exist)
    # Note: Server returns 500 when permission doesn't exist, not 404
    client.request("DELETE", api_url)

    # Create new permission with POST
    resp = client.post(api_url, json={"permission": permission})

    return resp.status_code in (200, 201)


def _set_group_prompt_permission(
    client: httpx.Client,
    prompt_name: str,
    group_name: str,
    permission: str,
) -> bool:
    """Set group-level prompt permission using RESTful API."""
    # New RESTful API path - prompts use their own endpoint
    api_url = f"api/2.0/mlflow/permissions/groups/{quote(group_name)}/prompts/{quote(prompt_name)}"

    # Delete existing permission first (ignore errors - permission may not exist)
    # Note: Server returns 500 when permission doesn't exist, not 404
    client.request("DELETE", api_url)

    # Create new permission with POST
    resp = client.post(api_url, json={"permission": permission})

    return resp.status_code in (200, 201)


def _create_run(client: httpx.Client, experiment_id: str) -> httpx.Response:
    """Attempt to create a run."""
    api = "api/2.0/mlflow/runs/create"
    payload = {
        "experiment_id": experiment_id,
        "start_time": int(time.time() * 1000),
        "tags": [{"key": "mlflow.runName", "value": f"group-test-run-{uuid.uuid4().hex[:8]}"}],
    }
    return client.post(api, json=payload)


def _get_experiment(client: httpx.Client, experiment_name: str) -> httpx.Response:
    """Get experiment by name."""
    api = f"ajax-api/2.0/mlflow/experiments/get-by-name?experiment_name={quote(experiment_name)}"
    return client.get(api)


def _get_model(client: httpx.Client, model_name: str) -> httpx.Response:
    """Get model by name."""
    api = f"ajax-api/2.0/mlflow/registered-models/get?name={quote(model_name)}"
    return client.get(api)


def _update_model(client: httpx.Client, model_name: str, description: str) -> httpx.Response:
    """Update model description."""
    api = "ajax-api/2.0/mlflow/registered-models/update"
    return client.patch(api, json={"name": model_name, "description": description})


def _delete_model(client: httpx.Client, model_name: str) -> httpx.Response:
    """Delete a model."""
    api = "ajax-api/2.0/mlflow/registered-models/delete"
    return client.request("DELETE", api, json={"name": model_name})


def _is_denied(status_code: int) -> bool:
    """Check if response indicates access denied."""
    return status_code in (401, 403, 404)


# =============================================================================
# Test Fixture: Create resources and set group permissions
# =============================================================================


@pytest.fixture(scope="module")
def group_permission_test_resources(
    base_url: str,
    ensure_server: None,
    http_client_factory,
    admin_cookies: httpx.Cookies,
    test_run_id: str,
):
    """Create resources and set group permissions.

    Groups and their permissions:
    - experiments-reader: READ
    - experiments-editor: EDIT
    - experiments-manager: MANAGE
    - experiments-no-access: NO_PERMISSIONS

    Users by group membership:
    - Alice: experiments-reader, models-reader, prompts-reader
    - Bob: experiments-editor, models-editor, prompts-editor
    - Charlie: experiments-manager, models-manager, prompts-manager
    - Dave: experiments-no-access, models-no-access, prompts-no-access
    - Eve: mlflow-users only (no specific resource groups - tests default permissions)
    """
    admin = "frank@example.com"

    experiment_name = f"group-perm-exp-{test_run_id}"
    model_name = f"group-perm-model-{test_run_id}"
    prompt_name = f"group-perm-prompt-{test_run_id}"

    with http_client_factory(admin) as client:
        # Create resources as admin
        exp_success, experiment_id = _create_experiment(client, experiment_name)
        assert exp_success, f"Failed to create experiment: {experiment_id}"

        assert _create_model(client, model_name), f"Failed to create model: {model_name}"
        assert _create_prompt(client, prompt_name, "Group permission test prompt"), f"Failed to create prompt"

        # Set group permissions for experiments
        assert _set_group_experiment_permission(client, experiment_id, "experiments-reader", "READ", base_url), "Failed to set experiments-reader permission"

        assert _set_group_experiment_permission(client, experiment_id, "experiments-editor", "EDIT", base_url), "Failed to set experiments-editor permission"

        assert _set_group_experiment_permission(
            client, experiment_id, "experiments-manager", "MANAGE", base_url
        ), "Failed to set experiments-manager permission"

        assert _set_group_experiment_permission(
            client, experiment_id, "experiments-no-access", "NO_PERMISSIONS", base_url
        ), "Failed to set experiments-no-access permission"

        # Set group permissions for models
        assert _set_group_model_permission(client, model_name, "models-reader", "READ"), "Failed to set models-reader permission"

        assert _set_group_model_permission(client, model_name, "models-editor", "EDIT"), "Failed to set models-editor permission"

        assert _set_group_model_permission(client, model_name, "models-manager", "MANAGE"), "Failed to set models-manager permission"

        assert _set_group_model_permission(client, model_name, "models-no-access", "NO_PERMISSIONS"), "Failed to set models-no-access permission"

        # Set group permissions for prompts
        assert _set_group_prompt_permission(client, prompt_name, "prompts-reader", "READ"), "Failed to set prompts-reader permission"

        assert _set_group_prompt_permission(client, prompt_name, "prompts-editor", "EDIT"), "Failed to set prompts-editor permission"

        assert _set_group_prompt_permission(client, prompt_name, "prompts-manager", "MANAGE"), "Failed to set prompts-manager permission"

        assert _set_group_prompt_permission(client, prompt_name, "prompts-no-access", "NO_PERMISSIONS"), "Failed to set prompts-no-access permission"

    return {
        "experiment_name": experiment_name,
        "experiment_id": experiment_id,
        "model_name": model_name,
        "prompt_name": prompt_name,
    }


# =============================================================================
# PERM-G-001 to PERM-G-004: Experiment Group Permissions
# =============================================================================


@pytest.mark.integration
def test_reader_group_can_view_experiment(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-001: User in experiments-reader group can view but not modify."""
    resources = group_permission_test_resources
    user = "alice@example.com"  # Member of experiments-reader

    with http_client_factory(user) as client:
        # Can view
        resp = _get_experiment(client, resources["experiment_name"])
        assert resp.status_code == 200, f"Reader should view experiment: {resp.status_code}"

        # Cannot create run
        run_resp = _create_run(client, resources["experiment_id"])
        assert _is_denied(run_resp.status_code), f"Reader should not create run: {run_resp.status_code}"


@pytest.mark.integration
def test_editor_group_can_modify_experiment(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-002: User in experiments-editor group can view and modify."""
    resources = group_permission_test_resources
    user = "bob@example.com"  # Member of experiments-editor

    with http_client_factory(user) as client:
        # Can view
        resp = _get_experiment(client, resources["experiment_name"])
        assert resp.status_code == 200, f"Editor should view experiment: {resp.status_code}"

        # Can create run
        run_resp = _create_run(client, resources["experiment_id"])
        assert run_resp.status_code == 200, f"Editor should create run: {run_resp.status_code}"


@pytest.mark.integration
def test_manager_group_has_full_control_experiment(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-003: User in experiments-manager group has full control."""
    resources = group_permission_test_resources
    user = "charlie@example.com"  # Member of experiments-manager

    with http_client_factory(user) as client:
        # Can view
        resp = _get_experiment(client, resources["experiment_name"])
        assert resp.status_code == 200, f"Manager should view experiment: {resp.status_code}"

        # Can create run
        run_resp = _create_run(client, resources["experiment_id"])
        assert run_resp.status_code == 200, f"Manager should create run: {run_resp.status_code}"


@pytest.mark.integration
def test_no_access_group_cannot_view_experiment(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-004: User in experiments-no-access group cannot access."""
    resources = group_permission_test_resources
    user = "dave@example.com"  # Member of experiments-no-access

    with http_client_factory(user) as client:
        # Cannot view
        resp = _get_experiment(client, resources["experiment_name"])
        assert _is_denied(resp.status_code), f"No-access should not view: {resp.status_code}"


# =============================================================================
# PERM-G-005 to PERM-G-008: Model Group Permissions
# =============================================================================


@pytest.mark.integration
def test_reader_group_can_view_model(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-005: User in models-reader group can view only."""
    resources = group_permission_test_resources
    user = "alice@example.com"  # Member of models-reader

    with http_client_factory(user) as client:
        # Can view
        resp = _get_model(client, resources["model_name"])
        assert resp.status_code == 200, f"Reader should view model: {resp.status_code}"

        # Cannot update
        update_resp = _update_model(client, resources["model_name"], "should not update")
        assert _is_denied(update_resp.status_code), f"Reader should not update: {update_resp.status_code}"


@pytest.mark.integration
def test_editor_group_can_modify_model(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-006: User in models-editor group can modify."""
    resources = group_permission_test_resources
    user = "bob@example.com"  # Member of models-editor

    with http_client_factory(user) as client:
        # Can view
        resp = _get_model(client, resources["model_name"])
        assert resp.status_code == 200, f"Editor should view model: {resp.status_code}"

        # Can update
        update_resp = _update_model(client, resources["model_name"], "updated by editor group")
        assert update_resp.status_code == 200, f"Editor should update: {update_resp.status_code}"


@pytest.mark.integration
def test_manager_group_has_full_control_model(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-007: User in models-manager group has full control."""
    resources = group_permission_test_resources
    user = "charlie@example.com"  # Member of models-manager

    with http_client_factory(user) as client:
        # Can view
        resp = _get_model(client, resources["model_name"])
        assert resp.status_code == 200, f"Manager should view model: {resp.status_code}"

        # Can update
        update_resp = _update_model(client, resources["model_name"], "updated by manager group")
        assert update_resp.status_code == 200, f"Manager should update: {update_resp.status_code}"


@pytest.mark.integration
def test_no_access_group_cannot_view_model(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-008: User in models-no-access group cannot access."""
    resources = group_permission_test_resources
    user = "dave@example.com"  # Member of models-no-access

    with http_client_factory(user) as client:
        # Cannot view
        resp = _get_model(client, resources["model_name"])
        assert _is_denied(resp.status_code), f"No-access should not view: {resp.status_code}"


# =============================================================================
# PERM-G-009 to PERM-G-012: Prompt Group Permissions
# =============================================================================


@pytest.mark.integration
def test_reader_group_can_view_prompt(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-009: User in prompts-reader group can view only."""
    resources = group_permission_test_resources
    user = "alice@example.com"  # Member of prompts-reader

    with http_client_factory(user) as client:
        # Can view (prompts are registered models)
        resp = _get_model(client, resources["prompt_name"])
        assert resp.status_code == 200, f"Reader should view prompt: {resp.status_code}"


@pytest.mark.integration
def test_editor_group_can_modify_prompt(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-010: User in prompts-editor group can modify."""
    resources = group_permission_test_resources
    user = "bob@example.com"  # Member of prompts-editor

    with http_client_factory(user) as client:
        # Can view
        resp = _get_model(client, resources["prompt_name"])
        assert resp.status_code == 200, f"Editor should view prompt: {resp.status_code}"


@pytest.mark.integration
def test_manager_group_has_full_control_prompt(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-011: User in prompts-manager group has full control."""
    resources = group_permission_test_resources
    user = "charlie@example.com"  # Member of prompts-manager

    with http_client_factory(user) as client:
        # Can view
        resp = _get_model(client, resources["prompt_name"])
        assert resp.status_code == 200, f"Manager should view prompt: {resp.status_code}"


@pytest.mark.integration
def test_no_access_group_cannot_view_prompt(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-012: User in prompts-no-access group cannot access."""
    resources = group_permission_test_resources
    user = "dave@example.com"  # Member of prompts-no-access

    with http_client_factory(user) as client:
        # Cannot view
        resp = _get_model(client, resources["prompt_name"])
        assert _is_denied(resp.status_code), f"No-access should not view: {resp.status_code}"


# =============================================================================
# PERM-G-013: Default Permission Test
# =============================================================================


@pytest.mark.integration
def test_user_without_specific_group_uses_default_permission(
    http_client_factory,
    group_permission_test_resources,
) -> None:
    """PERM-G-013: User without specific group membership uses DEFAULT_MLFLOW_PERMISSION.

    Eve only has mlflow-users group, no resource-specific groups.
    Access depends on DEFAULT_MLFLOW_PERMISSION setting (typically MANAGE).
    """
    resources = group_permission_test_resources
    user = "eve@example.com"  # Only in mlflow-users, no specific resource groups

    with http_client_factory(user) as client:
        # Access depends on default permission setting
        # If DEFAULT_MLFLOW_PERMISSION=MANAGE, Eve should have access
        # If DEFAULT_MLFLOW_PERMISSION=NO_PERMISSIONS, Eve should be denied
        resp = _get_experiment(client, resources["experiment_name"])

        # We accept either outcome - the test validates behavior is consistent
        # The key point is Eve's access is determined by default, not by group
        assert resp.status_code in (200, 401, 403, 404), f"Unexpected status: {resp.status_code}"

        if resp.status_code == 200:
            # Default allows access - verify user can interact
            # This indicates DEFAULT_MLFLOW_PERMISSION is permissive (likely MANAGE)
            pass
        else:
            # Default denies access - verify consistent denial
            # This indicates DEFAULT_MLFLOW_PERMISSION is restrictive (likely NO_PERMISSIONS)
            pass
