import ast
import inspect
import re

import pytest
from flask import current_app

from tests import service_json
from tests.conftest import (
    ORGANISATION_ID,
    ORGANISATION_TWO_ID,
    SERVICE_ONE_ID,
    SERVICE_TWO_ID,
)


@pytest.mark.parametrize(
    ("user_services", "user_organizations", "expected_status", "organization_checked"),
    [
        ([SERVICE_ONE_ID], [], 200, False),
        ([SERVICE_ONE_ID, SERVICE_TWO_ID], [], 200, False),
        ([], [ORGANISATION_ID], 200, True),
        ([SERVICE_ONE_ID], [ORGANISATION_ID], 200, False),
        ([], [], 403, True),
        ([SERVICE_TWO_ID], [], 403, True),
        ([SERVICE_TWO_ID], [ORGANISATION_ID], 200, True),
        ([SERVICE_ONE_ID, SERVICE_TWO_ID], [ORGANISATION_ID], 200, False),
        ([], [ORGANISATION_TWO_ID], 403, True),
        ([], [ORGANISATION_ID, ORGANISATION_TWO_ID], 200, True),
    ],
)
def test_services_pages_that_org_users_are_allowed_to_see(
    client_request,
    mocker,
    api_user_active,
    mock_get_annual_usage_for_service,
    mock_get_monthly_usage_for_service,
    mock_get_free_sms_fragment_limit,
    mock_get_monthly_notification_stats,
    mock_get_service,
    mock_get_invites_for_service,
    mock_get_users_by_service,
    mock_get_template_folders,
    mock_get_organization,
    mock_has_jobs,
    user_services,
    user_organizations,
    expected_status,
    organization_checked,
):
    api_user_active["services"] = user_services
    api_user_active["organizations"] = user_organizations
    api_user_active["permissions"] = {
        service_id: ["manage_users", "manage_settings"] for service_id in user_services
    }
    service = service_json(
        name="SERVICE WITH ORG",
        id_=SERVICE_ONE_ID,
        users=[api_user_active["id"]],
        organization_id=ORGANISATION_ID,
    )

    mock_get_service = mocker.patch(
        "app.notify_client.service_api_client.service_api_client.get_service",
        return_value={"data": service},
    )
    client_request.login(
        api_user_active,
        service=service if SERVICE_ONE_ID in user_services else None,
    )

    endpoints = (
        "main.usage",
        "main.manage_users",
    )

    for endpoint in endpoints:
        client_request.get(
            endpoint,
            service_id=SERVICE_ONE_ID,
            _expected_status=expected_status,
        )

    assert mock_get_service.called is organization_checked


def test_service_navigation_for_org_user(
    client_request,
    mocker,
    api_user_active,
    mock_get_annual_usage_for_service,
    mock_get_monthly_usage_for_service,
    mock_get_free_sms_fragment_limit,
    mock_get_monthly_notification_stats,
    mock_get_service,
    mock_get_invites_for_service,
    mock_get_users_by_service,
    mock_get_organization,
):
    api_user_active["services"] = []
    api_user_active["organizations"] = [ORGANISATION_ID]
    service = service_json(
        id_=SERVICE_ONE_ID,
        organization_id=ORGANISATION_ID,
    )
    mocker.patch("app.service_api_client.get_service", return_value={"data": service})
    client_request.login(api_user_active, service=service)

    page = client_request.get(
        "main.usage",
        service_id=SERVICE_ONE_ID,
    )
    assert [item.text.strip() for item in page.select("nav.nav a")] == [
        "Send messages",
        "Usage",
        "Team members",
    ]


@pytest.mark.parametrize(
    ("user_organizations", "expected_menu_items", "expected_status"),
    [
        (
            [],
            (
                "Send messages",
                "Sent messages",
                "Team members",
            ),
            403,
        ),
        (
            [ORGANISATION_ID],
            (
                "Send messages",
                "Sent messages",
                "Team members",
                "Usage",
            ),
            200,
        ),
    ],
)
def test_service_user_without_manage_service_permission_can_see_usage_page_when_org_user(
    client_request,
    mocker,
    active_caseworking_user,
    mock_has_no_jobs,
    mock_get_annual_usage_for_service,
    mock_get_monthly_usage_for_service,
    mock_get_free_sms_fragment_limit,
    mock_get_monthly_notification_stats,
    mock_get_service,
    mock_get_invites_for_service,
    mock_get_users_by_service,
    mock_get_organization,
    mock_get_service_templates,
    mock_get_template_folders,
    mock_get_api_keys,
    user_organizations,
    expected_status,
    expected_menu_items,
):
    active_caseworking_user["services"] = [SERVICE_ONE_ID]
    active_caseworking_user["organizations"] = user_organizations
    service = service_json(
        id_=SERVICE_ONE_ID,
        organization_id=ORGANISATION_ID,
    )
    mocker.patch("app.service_api_client.get_service", return_value={"data": service})
    client_request.login(active_caseworking_user, service=service)
    page = client_request.get(
        "main.choose_template",
        service_id=SERVICE_ONE_ID,
    )
    assert (
        tuple(item.text.strip() for item in page.select("nav.nav a"))
        == expected_menu_items
    )

    client_request.get(
        "main.usage",
        service_id=SERVICE_ONE_ID,
        _expected_status=expected_status,
    )


def get_name_of_decorator_from_ast_node(node):
    if isinstance(node, ast.Name):
        return str(node.id)
    if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
        return get_name_of_decorator_from_ast_node(node.func)
    if isinstance(node, ast.Attribute):
        return node.value.id
    return "{}.{}".format(node.func.value.id, node.func.attr)


def get_decorators_for_function(function):
    for node in ast.walk(ast.parse(inspect.getsource(function))):
        if isinstance(node, ast.FunctionDef):
            for decorator in node.decorator_list:
                yield get_name_of_decorator_from_ast_node(decorator)


SERVICE_ID_ARGUMENT = "service_id"
ORGANISATION_ID_ARGUMENT = "org_id"


def get_routes_and_decorators(argument_name=None):
    import app.main.views as views

    for module_name, module in inspect.getmembers(views):
        for function_name, function in inspect.getmembers(module):
            if inspect.isfunction(function):
                decorators = list(get_decorators_for_function(function))
                if "main.route" in decorators and (
                    not argument_name
                    or argument_name in inspect.signature(function).parameters.keys()
                ):
                    yield "{}.{}".format(module_name, function_name), decorators


def format_decorators(decorators, indent=8):
    return "\n".join(
        "{}@{}".format(" " * indent, decorator) for decorator in decorators
    )


def test_code_to_extract_decorators_works_with_known_examples():
    assert (
        "templates.choose_template",
        [
            "main.route",
            "main.route",
            "main.route",
            "main.route",
            "main.route",
            "main.route",
            "user_has_permissions",
        ],
    ) in list(get_routes_and_decorators(SERVICE_ID_ARGUMENT))
    assert (
        "organizations.organization_dashboard",
        ["main.route", "user_has_permissions"],
    ) in list(get_routes_and_decorators(ORGANISATION_ID_ARGUMENT))
    assert (
        "platform_admin.platform_admin",
        ["main.route", "user_is_platform_admin"],
    ) in list(get_routes_and_decorators())


def test_routes_have_permissions_decorators():
    for endpoint, decorators in list(
        get_routes_and_decorators(SERVICE_ID_ARGUMENT)
    ) + list(get_routes_and_decorators(ORGANISATION_ID_ARGUMENT)):
        file, function = endpoint.split(".")

        assert "user_is_logged_in" not in decorators, (
            "@user_is_logged_in used on service or organization specific endpoint\n"
            "Use @user_has_permissions() or @user_is_platform_admin only\n"
            "app/main/views/{}.py::{}\n"
        ).format(file, function)

        if "user_is_platform_admin" in decorators:
            continue

        assert "user_has_permissions" in decorators, (
            "Missing @user_has_permissions decorator\n"
            "Use @user_has_permissions() or @user_is_platform_admin instead\n"
            "app/main/views/{}.py::{}\n"
        ).format(file, function)

    for _endpoint, decorators in get_routes_and_decorators():
        assert "login_required" not in decorators, (
            "@login_required found\n"
            "For consistency, use @user_is_logged_in() instead (from app.utils)\n"
            "app/main/views/{}.py::{}\n"
        ).format(file, function)

        if "user_is_platform_admin" in decorators:
            assert "user_has_permissions" not in decorators, (
                "@user_has_permissions and @user_is_platform_admin decorating same function\n"
                "You can only use one of these at a time\n"
                "app/main/views/{}.py::{}\n"
            ).format(file, function)
            assert "user_is_logged_in" not in decorators, (
                "@user_is_logged_in used with @user_is_platform_admin\n"
                "Use @user_is_platform_admin only\n"
                "app/main/views/{}.py::{}\n"
            ).format(file, function)


def test_routes_require_uuids(client_request):
    for rule in current_app.url_map.iter_rules():
        for param in re.findall("<([^>]*)>", rule.rule):
            if "_id" in param and not param.startswith("uuid:"):
                pytest.fail(("Should be <uuid:{}> in {}").format(param, rule.rule))
