from __future__ import annotations

from os import environ
from typing import Dict, Optional, Tuple
from unittest import mock

import coiled
import dask
import pytest
import structlog
from coiled.compatibility import COILED_VERSION
from coiled.core import Cloud, _parse_gcp_creds
from coiled.exceptions import (
    AccountConflictError,
    AWSCredentialsParameterError,
    GCPCredentialsError,
    GCPCredentialsParameterError,
    RegistryParameterError,
    UnsupportedBackendError,
)
from django.conf import settings

from api_tokens.models import ApiToken
from backends import types
from backends.utils import parse_gcp_location
from interactions.models import UserInteraction
from software_environments.type_defs import ContainerRegistryType

from ..errors import ServerError
from ..exceptions import ParseIdentifierError

pytestmark = [
    pytest.mark.django_db(transaction=True),
]

logger = structlog.get_logger(__name__)

DASKDEV_IMAGE = environ.get("DASKDEV_IMAGE", "daskdev/dask:latest")
DASKDEV_IMAGE = "daskdev/dask:latest"


@pytest.mark.asyncio
async def test_version_error(
    base_user, remote_access_url, monkeypatch, base_user_token
):
    with dask.config.set(
        {
            "coiled.user": base_user.user.username,
            "coiled.token": base_user_token,
            "coiled.server": remote_access_url,
            "coiled.account": base_user.account.name,
            "coiled.no-minimum-version-check": False,
        }
    ):
        monkeypatch.setattr(coiled.core, "COILED_VERSION", "0.0.14")
        with pytest.raises(ServerError, match="Coiled now requires"):
            async with coiled.Cloud(asynchronous=True):
                pass


@pytest.mark.asyncio
async def test_basic(sample_user):
    async with coiled.Cloud(
        asynchronous=True,
    ) as cloud:

        assert cloud.user == sample_user.user.username
        assert sample_user.user.username in cloud.accounts
        assert cloud.default_account == sample_user.user.username


@pytest.mark.asyncio
async def test_trailing_slash(remote_access_url, sample_user):
    async with coiled.Cloud(
        server=remote_access_url + "/",
        asynchronous=True,
    ):
        pass


@pytest.mark.asyncio
async def test_server_input(remote_access_url, sample_user):
    async with coiled.Cloud(
        server=remote_access_url.split("://")[-1],
        asynchronous=True,
    ) as cloud:
        assert cloud.user == sample_user.user.username
        assert sample_user.user.username in cloud.accounts
        assert cloud.default_account == sample_user.user.username


@pytest.mark.asyncio
async def test_informative_error_org(remote_access_url, sample_user):
    with pytest.raises(PermissionError) as info:
        async with coiled.Cloud(
            server=remote_access_url.split("://")[-1],
            account="does-not-exist",
            asynchronous=True,
        ):
            pass

    assert sample_user.account.slug in str(info.value)
    assert "does-not-exist" in str(info.value)


@pytest.mark.asyncio
async def test_config(remote_access_url, sample_user, sample_user_token):
    async with coiled.Cloud(
        user=sample_user.user.username,
        token=sample_user_token,
        server=remote_access_url,
        asynchronous=True,
    ) as cloud:
        assert cloud.user == sample_user.user.username
        assert sample_user.user.username in cloud.accounts
        assert cloud.default_account == sample_user.user.username


def test_config_attribute():
    assert coiled.config == dask.config.get("coiled")


@pytest.mark.asyncio
async def test_repr(remote_access_url, sample_user):
    async with coiled.Cloud(asynchronous=True) as cloud:
        for func in [str, repr]:
            assert sample_user.user.username in func(cloud)
            assert remote_access_url in func(cloud)


@pytest.mark.asyncio
async def test_normalize_name(cloud, cleanup):
    assert cloud._normalize_name(name="foo/bar") == ("foo", "bar")
    assert cloud._normalize_name(name="bar") == (cloud.default_account, "bar")
    assert cloud._normalize_name(name="bar", context_account="baz") == ("baz", "bar")

    # Invalid name raises
    with pytest.raises(ParseIdentifierError):
        cloud._normalize_name(name="foo/bar/baz")

    # Will throw error if we tell it that only one
    # account makes sense.
    with pytest.raises(AccountConflictError):
        cloud._normalize_name(
            name="foo/bar", context_account="baz", raise_on_account_conflict=True
        )
    # Doesn't raise if account over-specified but there's not conflict
    assert cloud._normalize_name(
        name="foo/bar", context_account="foo", raise_on_account_conflict=True
    )


@pytest.mark.asyncio
async def test_normalize_name_uppercase_account(cloud, cleanup):
    # Users that have uppercase characters in the name will get cluster
    # name with <account>-<identifier>
    cluster_name = "BobTheBuilder-940a3351-b"
    assert cloud._normalize_name(name=cluster_name, allow_uppercase=True) == (
        cloud.default_account,
        cluster_name,
    )

    assert cloud._normalize_name(name="bar", context_account="baz") == ("baz", "bar")

    # Invalid name raises
    with pytest.raises(ParseIdentifierError):
        cloud._normalize_name(name="foo/bar/baz")

    # Will throw error if we tell it that only one
    # account makes sense.
    with pytest.raises(AccountConflictError):
        cloud._normalize_name(
            name="foo/bar", context_account="baz", raise_on_account_conflict=True
        )
    # Doesn't raise if account over-specified but there's not conflict
    assert cloud._normalize_name(
        name="foo/bar", context_account="foo", raise_on_account_conflict=True
    )


@pytest.mark.asyncio
async def test_default_account(sample_user):
    async with coiled.Cloud(
        asynchronous=True,
    ) as cloud:
        assert cloud.accounts
        assert cloud.default_account == sample_user.user.username


@pytest.mark.asyncio
async def test_cloud_repr_html(cloud, cleanup):
    text = cloud._repr_html_()
    assert cloud.user in text
    assert cloud.server in text
    assert cloud.default_account in text


@pytest.mark.asyncio
async def test_default_org_username(second_user):
    async with coiled.Cloud(asynchronous=True) as cloud:
        assert cloud.default_account == second_user.user.username


@pytest.mark.asyncio
async def test_account_config(sample_user, second_account):
    with dask.config.set({"coiled.account": second_account.account.slug}):
        async with coiled.Cloud(
            asynchronous=True,
        ) as cloud:
            assert cloud.default_account == second_account.account.slug


@pytest.mark.test_group("core-slow-group-9")
def test_public_api_software_environments(sample_user):
    results = coiled.list_software_environments()
    assert not results

    name = "foo"
    coiled.create_software_environment(name=name, container=DASKDEV_IMAGE)
    results = coiled.list_software_environments()
    assert len(results) == 1
    expected_env_name = f"{sample_user.account.name}/foo"
    assert expected_env_name in results
    assert results[expected_env_name]["container"] == DASKDEV_IMAGE

    coiled.delete_software_environment(name)
    results = coiled.list_software_environments()
    assert not results


@pytest.mark.django_db
def test_public_api_depagination_with_api_token(sample_user, software_env):

    # create 101 api tokens
    ApiToken.objects.bulk_create(
        [
            ApiToken(
                user=sample_user.user,
                token_hash="aaa",
            )
            for i in range(101)
        ]
    )

    results = coiled.list_api_tokens()
    expected_num_results = ApiToken.objects.filter(user=sample_user.user).count()

    assert len(results) == expected_num_results

    # create another 101 api tokens
    ApiToken.objects.bulk_create(
        [
            ApiToken(
                user=sample_user.user,
                token_hash="aaa",
            )
            for i in range(101, 202)
        ]
    )

    results = coiled.list_api_tokens()
    expected_num_results = ApiToken.objects.filter(user=sample_user.user).count()
    logger.info(
        f"checking depaginated results from list_api_tokens, expected: "
        f"{expected_num_results}, found: {len(results)}"
    )
    assert len(results) == expected_num_results


@pytest.mark.asyncio
@pytest.mark.django_db
async def test_public_api_depagination(sample_user, software_env):

    cloud_client = Cloud(sample_user.account.slug)
    PAGE_SIZE = 100
    NUM_PAGES = 8

    async def mock_page_func(page, account=None) -> Tuple[Dict, Optional[str]]:
        assert account == sample_user.account.slug
        start = ((page - 1) * PAGE_SIZE) + 1
        end = page * PAGE_SIZE
        page_result = {i: i for i in range(start, end + 1)}
        next_url = "dummy_url" if page < NUM_PAGES else None
        return page_result, next_url

    results = await cloud_client._depaginate(
        mock_page_func, account=sample_user.account.slug
    )
    expected_num_results = PAGE_SIZE * NUM_PAGES

    assert len(results) == expected_num_results
    assert len(set(results.keys())) == expected_num_results


def test_public_api_list_core_usage_json(sample_user):
    result = coiled.list_core_usage()

    assert result["core_limit_account"] == 10
    assert result["core_limit_user"] == 10
    assert result["num_running_cores_user"] == 0
    assert result["num_running_cores_account"] == 0


def test_public_api_diagnostics(sample_user):
    result = coiled.diagnostics()

    assert result["health_check"]
    assert result["local_versions"]
    assert result["coiled_configuration"]


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_invalid_args(
    cloud,
    sample_user,
):
    with pytest.raises(UnsupportedBackendError) as e_info:
        coiled.set_backend_options(backend="vm_geocities")  # type: ignore
    assert "Supplied backend: vm_geocities not in supported types: " in str(e_info)
    assert "aws" in str(e_info)
    assert "gcp" in str(e_info)


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_aws_vm_customer_hosted(cloud, sample_user, mocker):

    mocker.patch("coiled.utils.boto3")
    account = sample_user.account

    # mock this _configure_backend method as we don't want to test this here
    _configure_backend_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager._configure_backend"
    )

    # create VPC requires have aws_* creds
    with pytest.raises(AWSCredentialsParameterError) as e_info:
        coiled.set_backend_options(backend="aws")
    assert "aws_access_key_id" in str(e_info)
    assert "aws_secret_access_key" in str(e_info)

    credentials = {
        "aws_access_key_id": "test-aws_access_key_id",
        "aws_secret_access_key": "test-aws_secret_access_key",
    }

    # set region
    coiled.set_backend_options(
        backend="aws",
        aws_region=settings.AWS_DEFAULT_USER_REGION,
        **credentials,
    )
    account.refresh_from_db()

    options = {
        "aws_region_name": settings.AWS_DEFAULT_USER_REGION,
        "credentials": {
            "aws_access_key": "test-aws_access_key_id",
            "aws_secret_key": "test-aws_secret_access_key",
        },
        # these are optional, but default values will be reflected
        "firewall": {},
        "firewall_spec": {},
        "network": {},
        "provider_name": "aws",
        "type": "aws_cloudbridge_backend_options",
    }

    assert account.options == options

    assert account.container_registry == {
        "type": ContainerRegistryType.ECR,
        "credentials": {
            "aws_access_key_id": "test-aws_access_key_id",
            "aws_secret_access_key": "test-aws_secret_access_key",
        },
        "public_ecr": False,
        "region": settings.AWS_DEFAULT_USER_REGION,
    }
    assert account.backend == types.BackendChoices.VM
    assert _configure_backend_mock.called

    kwargs = _configure_backend_mock.call_args.kwargs

    kwargs_user_info = kwargs["user_info"]
    kwargs_options = kwargs["options"]

    assert kwargs_user_info.user_id == sample_user.user.id
    assert kwargs_user_info.account_slug == account.slug
    assert kwargs_options.type == options["type"]
    assert kwargs_options.aws_region_name == options["aws_region_name"]


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_gcp_vm_customer_hosted(cloud, sample_user, mocker):

    account = sample_user.account

    _configure_backend_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager._configure_backend"
    )

    reqed_keys = [
        "type",
        "project_id",
        "private_key_id",
        "private_key",
        "client_email",
        "client_id",
        "auth_uri",
        "token_uri",
        "auth_provider_x509_cert_url",
        "client_x509_cert_url",
    ]

    gcp_service_creds_dict = {k: "token" for k in reqed_keys}

    # no zone set
    coiled.set_backend_options(
        backend="gcp",
        customer_hosted=True,
        gcp_service_creds_dict=gcp_service_creds_dict,
        gcp_project_id="test-project-name",
        gcp_region="gcp_region_name",
    )
    account.refresh_from_db()

    options = {
        "provider_name": "gcp",
        "type": "gcp_cloudbridge_backend_options",
        "gcp_project_name": "test-project-name",
        "gcp_region_name": "gcp_region_name",
        "gcp_zone_name": None,
        "gcp_service_creds_dict": gcp_service_creds_dict,
        "firewall": {},
        "firewall_spec": {},
        "network": {},
        "instance_service_account": None,
    }
    assert account.options == options
    assert account.container_registry == {
        "type": ContainerRegistryType.ECR,
        "credentials": {},
        "public_ecr": False,
        "region": settings.AWS_DEFAULT_USER_REGION,
    }
    assert account.backend == types.BackendChoices.VM
    assert _configure_backend_mock.called

    kwargs = _configure_backend_mock.call_args.kwargs

    kwargs_user_info = kwargs["user_info"]
    kwargs_options = kwargs["options"]

    assert kwargs_user_info.user_id == sample_user.user.id
    assert kwargs_user_info.account_slug == account.slug
    assert kwargs_options.type == options["type"]
    assert kwargs_options.gcp_region_name == options["gcp_region_name"]

    # zone set
    coiled.set_backend_options(
        backend="gcp",
        customer_hosted=True,
        gcp_service_creds_dict=gcp_service_creds_dict,
        gcp_project_id="test-project-name",
        gcp_region="gcp_region_name",
        gcp_zone="gcp_region_name-b",
    )
    account.refresh_from_db()
    options = {
        "provider_name": "gcp",
        "type": "gcp_cloudbridge_backend_options",
        "gcp_project_name": "test-project-name",
        "gcp_region_name": "gcp_region_name",
        "gcp_zone_name": "gcp_region_name-b",
        "gcp_service_creds_dict": gcp_service_creds_dict,
        "firewall": {},
        "firewall_spec": {},
        "network": {},
        "instance_service_account": None,
    }
    assert account.options == options


@pytest.mark.test_group("core-slow-group-12")
def test__parse_gcp_creds_missing_project_id(cloud, sample_user, mocker):
    reqed_keys = [
        "type",
        "private_key_id",
        "private_key",
        "client_email",
        "client_id",
        "auth_uri",
        "token_uri",
        "auth_provider_x509_cert_url",
        "client_x509_cert_url",
    ]

    gcp_service_creds_dict = {k: "token" for k in reqed_keys}

    with pytest.raises(GCPCredentialsError) as e:
        _parse_gcp_creds(
            gcp_service_creds_file=None, gcp_service_creds_dict=gcp_service_creds_dict
        )
    error_message = e.value.args[0]
    assert error_message.startswith(
        "Unable to find 'project_id' in 'gcp_service_creds_dict'"
    )


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_gcp_vm_customer_hosted_gar_registry(
    cloud, sample_user, mocker
):

    account = sample_user.account

    _configure_backend_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager._configure_backend"
    )
    mocker.patch(
        "software_environments.registry.gcp",
        mock.AsyncMock(),
    )

    reqed_keys = [
        "type",
        "project_id",
        "private_key_id",
        "private_key",
        "client_email",
        "client_id",
        "auth_uri",
        "token_uri",
        "auth_provider_x509_cert_url",
        "client_x509_cert_url",
    ]

    gcp_service_creds_dict = {k: "token" for k in reqed_keys}

    coiled.set_backend_options(
        backend="gcp",
        customer_hosted=True,
        gcp_service_creds_dict=gcp_service_creds_dict,
        gcp_project_id="test-project-name",
        gcp_region="gcp_region_name",
        gcp_zone="gcp_zone_name",
        registry_type="gar",
    )
    account.refresh_from_db()

    options = {
        "provider_name": "gcp",
        "type": "gcp_cloudbridge_backend_options",
        "gcp_project_name": "test-project-name",
        "gcp_region_name": "gcp_region_name",
        "gcp_zone_name": "gcp_zone_name",
        "gcp_service_creds_dict": gcp_service_creds_dict,
        "firewall": {},  # These are added at the account level regardless
        "firewall_spec": {},
        "network": {},
        "instance_service_account": None,
    }
    assert account.options == options
    assert account.container_registry == {
        "type": ContainerRegistryType.GAR,
        "credentials": gcp_service_creds_dict,
        "project_id": "test-project-name",
        "location": "gcp_region_name",
    }
    assert account.backend == types.BackendChoices.VM
    assert _configure_backend_mock.called

    kwargs = _configure_backend_mock.call_args.kwargs

    kwargs_user_info = kwargs["user_info"]
    kwargs_options = kwargs["options"]

    assert kwargs_user_info.user_id == sample_user.user.id
    assert kwargs_user_info.account_slug == account.slug
    assert kwargs_options.type == options["type"]
    assert kwargs_options.gcp_region_name == options["gcp_region_name"]


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_gar_registry_validation(cloud, mocker):
    mocker.patch("coiled.utils.boto3")
    mocker.patch("backends.cloudbridge.cloudbridge.ClusterManager._configure_backend")
    credentials = {
        "aws_access_key_id": "test-aws_access_key_id",
        "aws_secret_access_key": "test-aws_secret_access_key",
    }
    reqed_keys = [
        "type",
        "private_key_id",
        "private_key",
        "client_email",
        "client_id",
        "auth_uri",
        "token_uri",
        "auth_provider_x509_cert_url",
        "client_x509_cert_url",
    ]

    gcp_service_creds_dict = {k: "token" for k in reqed_keys}

    with pytest.raises(GCPCredentialsError) as e:
        coiled.set_backend_options(
            gcp_service_creds_dict=gcp_service_creds_dict,
            # These are required for GAR
            # gcp_project_name="test-project-name",
            # gcp_region_name="gcp_region_name",
            registry_type="gar",
            **credentials,
        )
    error_message = e.value.args[0]
    assert error_message.startswith(
        "Unable to find 'project_id' in 'gcp_service_creds_dict'"
    )

    with pytest.raises(GCPCredentialsParameterError) as e:
        coiled.set_backend_options(
            backend="gcp",
            customer_hosted=True,
            # Required for GAR, but error is different because
            # it happens earlier.
            # gcp_service_creds_dict=gcp_service_creds_dict,
            gcp_project_name="test-project-name",
            gcp_region="gcp_region_name",
            registry_type="gar",
            **credentials,
        )
    error_message = e.value.args[0]
    assert (
        "Parameter 'gcp_service_creds_file' or 'gcp_service_creds_dict' must be supplied"
        in error_message
    )


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_handle_exception_in_configure_backend(
    cloud, sample_user, mocker
):

    account = sample_user.account

    assert account.options == {"aws_region_name": "us-east-1"}

    assert account.container_registry == {
        "type": ContainerRegistryType.ECR,
        "credentials": {},
        "public_ecr": False,
        "region": settings.AWS_DEFAULT_USER_REGION,
    }
    assert account.backend == settings.DEFAULT_CLUSTER_BACKEND

    configure_backend_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager._configure_backend"
    )
    configure_backend_mock.side_effect = Exception("boom something broke")

    rollback_failed_configure_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager.rollback_failed_configure"
    )

    reqed_keys = [
        "type",
        "project_id",
        "private_key_id",
        "private_key",
        "client_email",
        "client_id",
        "auth_uri",
        "token_uri",
        "auth_provider_x509_cert_url",
        "client_x509_cert_url",
    ]

    gcp_service_creds_dict = {k: "token" for k in reqed_keys}

    with pytest.raises(ServerError):
        coiled.set_backend_options(
            backend="gcp",
            customer_hosted=True,
            gcp_service_creds_dict=gcp_service_creds_dict,
            gcp_project_name="test-project-name",
            gcp_region="gcp_region_name",
        )
    account.refresh_from_db()
    assert account.options == {"aws_region_name": "us-east-1"}

    assert account.container_registry == {
        "type": ContainerRegistryType.ECR,
        "credentials": {},
        "public_ecr": False,
        "region": settings.AWS_DEFAULT_USER_REGION,
    }
    assert account.backend == settings.DEFAULT_CLUSTER_BACKEND
    assert rollback_failed_configure_mock.called


### TEST backend_options_registries


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_registry_ecr(cloud, sample_user, mocker):
    # pytest installs the coiled client
    mocker.patch("coiled.utils.boto3")
    account = sample_user.account
    # We don't really want to configure the backend
    _configure_backend_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager._configure_backend"
    )
    credentials = {
        "aws_access_key_id": "test-aws_access_key_id",
        "aws_secret_access_key": "test-aws_secret_access_key",
    }

    coiled.set_backend_options(registry_type="ecr", **credentials)

    account.refresh_from_db()
    assert account.options == {
        "aws_region_name": settings.AWS_DEFAULT_USER_REGION,
        "credentials": {
            "aws_access_key": "test-aws_access_key_id",
            "aws_secret_key": "test-aws_secret_access_key",
        },
        "firewall": {},
        "firewall_spec": {},
        "network": {},
        "provider_name": "aws",
        "type": "aws_cloudbridge_backend_options",
    }
    assert account.container_registry == {
        "type": ContainerRegistryType.ECR,
        "credentials": credentials,
        "public_ecr": False,
        "region": settings.AWS_DEFAULT_USER_REGION,
    }
    assert account.backend == types.BackendChoices.VM
    assert _configure_backend_mock.called


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_registry_dockerhub(
    cloud,
    sample_user,
    mocker,
):
    account = sample_user.account

    # pytest installs the coiled client
    mocker.patch("coiled.utils.boto3")
    _configure_backend_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager._configure_backend"
    )
    credentials = {
        "aws_access_key_id": "test-aws_access_key_id",
        "aws_secret_access_key": "test-aws_secret_access_key",
    }

    registry = {
        "type": ContainerRegistryType.DOCKER_HUB,
        "namespace": "registry_namespace",
        "access_token": "registry_access_token",
        "uri": "registry_uri",
        "username": "registry_username",
    }
    kwargs = {f"registry_{k}": v for k, v in registry.items()}

    coiled.set_backend_options(**kwargs, **credentials)

    account.refresh_from_db()

    registry["account"] = registry["namespace"]
    registry["password"] = registry["access_token"]
    del registry["namespace"]
    del registry["access_token"]

    assert account.container_registry == registry
    assert account.backend == types.BackendChoices.VM

    ## test use register_username if not registry_namespace
    registry = {
        "type": ContainerRegistryType.DOCKER_HUB,
        "access_token": "registry_access_token",
        "uri": "registry_uri",
        "username": "registry_username",
    }
    kwargs = {f"registry_{k}": v for k, v in registry.items()}

    coiled.set_backend_options(**kwargs, **credentials)

    account.refresh_from_db()

    registry["account"] = registry["username"]
    registry["password"] = registry["access_token"]
    del registry["access_token"]
    assert account.container_registry == registry
    assert account.backend == types.BackendChoices.VM
    assert _configure_backend_mock.called


@pytest.mark.test_group("core-slow-group-12")
def test_set_backend_options_registry_dockerhub_required_fields(
    cloud, sample_user, mocker
):
    # pytest installs the coiled client
    mocker.patch("coiled.utils.boto3")
    mocker.patch("backends.cloudbridge.cloudbridge.ClusterManager._configure_backend")
    credentials = {
        "aws_access_key_id": "test-aws_access_key_id",
        "aws_secret_access_key": "test-aws_secret_access_key",
    }

    registry = {
        "type": ContainerRegistryType.DOCKER_HUB,
        "namespace": "registry_namespace",
        "access_token": "registry_access_token",
        "uri": "registry_uri",
    }
    kwargs = {f"registry_{k}": v for k, v in registry.items()}

    with pytest.raises(RegistryParameterError) as e_info:
        coiled.set_backend_options(
            **kwargs, registry_username="UpperCasedUserName", **credentials
        )
    assert "Your dockerhub [registry_username] must be lowercase" in str(e_info)

    with pytest.raises(RegistryParameterError) as e_info:
        coiled.set_backend_options(**kwargs, **credentials)
    assert (
        "For setting your registry credentials, these fields cannot be empty: ['registry_username']"
        in str(e_info)
    )


def test_parse_gcp_creds_parameter_missing():
    with pytest.raises(GCPCredentialsParameterError):
        _parse_gcp_creds(gcp_service_creds_dict=None, gcp_service_creds_file="")


def test_parse_gcp_creds_not_file():
    with pytest.raises(GCPCredentialsError):
        _parse_gcp_creds(
            gcp_service_creds_dict=None, gcp_service_creds_file="non_existent"
        )


@pytest.mark.test_group("core-slow-group-12")
def test_parse_gcp_location(mocker, cloud, sample_user):
    account = sample_user.account

    default_region, default_zone = parse_gcp_location(settings.GCP_DEFAULT_USER_ZONE)

    configure_backend_mock = mocker.patch(
        "backends.cloudbridge.cloudbridge.ClusterManager._configure_backend"
    )
    mocker.patch(
        "software_environments.registry.gcp",
        mock.AsyncMock(),
    )

    reqed_keys = [
        "type",
        "project_id",
        "private_key_id",
        "private_key",
        "client_email",
        "client_id",
        "auth_uri",
        "token_uri",
        "auth_provider_x509_cert_url",
        "client_x509_cert_url",
    ]

    gcp_service_creds_dict = {k: "token" for k in reqed_keys}

    coiled.set_backend_options(
        backend="gcp",
        customer_hosted=True,
        gcp_service_creds_dict=gcp_service_creds_dict,
        gcp_project_id="test-project-name",
        gcp_region=default_region,
        gcp_zone=default_zone,
        registry_type="gar",
    )
    account.refresh_from_db()

    assert account.options.get("gcp_region_name") == default_region
    assert account.options.get("gcp_zone_name") == default_zone

    assert configure_backend_mock.called


def test_parse_gcp_creds_bad_file(mocker, tmp_path):
    test_file = tmp_path / "test.txt"
    test_file.write_text("test")
    mocker.mock_open(read_data="test").return_value = ""
    with pytest.raises(GCPCredentialsError):
        result = _parse_gcp_creds(
            gcp_service_creds_dict=None, gcp_service_creds_file=test_file
        )
        assert "not a valid JSON file" in result


def test_parse_gcp_creds_missing_all_keys(mocker, tmp_path):
    test_file = tmp_path / "test.json"
    test_file.write_text("test")
    mocker.patch("json.load").return_value = {}

    with pytest.raises(GCPCredentialsError) as error:
        _parse_gcp_creds(gcp_service_creds_dict=None, gcp_service_creds_file=test_file)

    assert "is missing the keys" in str(error.value)


def test_parse_gcp_creds_missing_project_id(mocker, tmp_path):
    test_file = tmp_path / "test.json"
    test_file.write_text("test")
    mocker.patch("json.load").return_value = {
        "type": "",
        "private_key_id": "",
        "private_key": "",
        "client_email": "",
        "client_id": "",
        "auth_uri": "",
        "token_uri": "",
        "auth_provider_x509_cert_url": "",
        "client_x509_cert_url": "",
    }

    with pytest.raises(GCPCredentialsError) as error:
        _parse_gcp_creds(gcp_service_creds_dict=None, gcp_service_creds_file=test_file)

    assert "is missing the keys" in str(error.value)
    assert "project_id" in str(error.value)


def test_list_gpu_types(sample_user, capfd):
    gpu_type = coiled.list_gpu_types()

    assert "nvidia-tesla-t4" in gpu_type.values()


def test_public_api_get_billing_activity(sample_user):
    result = coiled.get_billing_activity()

    assert result["count"] == 3
    assert result["next"] is None
    assert result["page_size"] == 15
    for res in result["results"]:
        assert (
            res["kind"] in ["program_change", "monthly_grant", "activate_account"]
        ) is True
        if res["kind"] == "monthly_grant":
            assert res["amount_credits"] == "10000.0000"


@pytest.mark.django_db
def test_add_interaction(sample_user):
    with Cloud() as cloud:
        cloud.add_interaction(action="test", success=True, additional_text="Add this")

    interaction = UserInteraction.objects.first()
    assert interaction.action == "test"
    assert interaction.version == 1
    assert interaction.success
    assert interaction.coiled_version == COILED_VERSION
    assert interaction.additional_text == "Add this"
