# Copyright 2023 Northern.tech AS
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
import pytest
import multiprocessing as mp
import json
import os
import random
import time
import uuid
import logging

from base64 import urlsafe_b64decode
from datetime import datetime, timedelta

from pymongo import MongoClient

import testutils.api.deviceauth as deviceauth
import testutils.api.useradm as useradm
import testutils.api.inventory as inventory
import testutils.api.inventory_v2 as inventory_v2
import testutils.api.deployments as deployments
import testutils.api.deployments_v2 as deployments_v2

from testutils.api.client import ApiClient
from testutils.common import (
    create_org,
    create_user,
    get_mender_artifact,
    make_accepted_devices,
    make_device_with_inventory,
    submit_inventory,
    useExistingTenant,
)
from testutils.infra.container_manager.kubernetes_manager import isK8S

WAITING_MULTIPLIER = 8 if isK8S() else 1
WAITING_TIME_K8S = 5.0
logger = logging.getLogger("test_deployments")


def upload_image(filename, auth_token, description="abc"):
    api_client = ApiClient(deployments.URL_MGMT)
    api_client.headers = {}
    r = api_client.with_auth(auth_token).call(
        "POST",
        deployments.URL_DEPLOYMENTS_ARTIFACTS,
        files=(
            ("description", (None, description)),
            ("size", (None, str(os.path.getsize(filename)))),
            ("artifact", (filename, open(filename, "rb"), "application/octet-stream")),
        ),
    )
    assert r.status_code == 201


def create_test_artifacts(
    utoken: str,
    mongo_client: MongoClient,
    number_artifacts: int = 3,
    name: str = "test-artifact",
    description: str = "abc",
):
    if number_artifacts < 1:
        return []

    artifact_names = []
    claims_b64 = utoken.split(".")[1]
    claims_b64 += "=" * ((3 - len(claims_b64)) % 3)  # Add padding
    claims = json.loads(urlsafe_b64decode(claims_b64).decode())
    tenant_id = claims.get("mender.tenant")

    with get_mender_artifact(
        "test-artifact",
    ) as artifact:
        upload_image(artifact, utoken, description)
        artifact_names.append("test-artifact")

    for i in range(number_artifacts - 1):
        artifact_name = f"{name}-{i}"
        description = f"{description}-{i}"
        devicetype = f"arm1-{i}"
        with get_mender_artifact(
            artifact_name,
            device_types=[devicetype],
        ) as artifact:
            upload_image(artifact, utoken, description)
            artifact_names.append(artifact_name)

    db_name = f"deployment_service-{tenant_id}" if tenant_id else "deployment_service"
    artifacts = [
        {
            "name": doc["meta_artifact"]["name"],
            "description": doc["meta"].get("description"),
            "device_types": doc["meta_artifact"]["depends"]["device_type"],
        }
        for doc in mongo_client[db_name]
        .images.find(
            {},
            {
                "meta_artifact.name": 1,
                "meta_artifact.depends.device_type": 1,
                "meta.description": 1,
            },
        )
        .sort({"meta_artifact.name": 1})
    ]

    return artifacts


def compare_artifact_names(artifacts, expected_names):
    assert len(artifacts) == len(expected_names)

    actual_names = [artifact["name"] for artifact in artifacts]
    assert expected_names == actual_names


def create_tenant_test_setup(
    user_name, tenant_name, nr_deployments=3, nr_devices=100, plan="os"
):
    """
    Creates a tenant, and a user belonging to the tenant
    with 'nr_deployments', and 'nr_devices'
    """
    api_mgmt_deploy = ApiClient(deployments.URL_MGMT)
    tenant = create_org(tenant_name, user_name, "correcthorse", plan=plan)
    user = tenant.users[0]
    r = ApiClient(useradm.URL_MGMT).call(
        "POST", useradm.URL_LOGIN, auth=(user.name, user.pwd)
    )
    assert r.status_code == 200
    user.utoken = r.text
    tenant.users = [user]
    upload_image("/tests/data/test-artifact.mender", user.utoken)

    # count deployments
    resp = api_mgmt_deploy.with_auth(user.utoken).call("GET", "/deployments")
    assert resp.status_code == 200
    count = int(resp.headers["X-Total-Count"])

    # Create three deployments for the user
    for i in range(nr_deployments):
        request_body = {
            "name": str(i) + "st-dummy-deployment",
            "artifact_name": "deployments-phase-testing",
            "devices": ["uuid not needed" + str(i) for i in range(nr_devices)],
        }
        resp = api_mgmt_deploy.with_auth(user.utoken).call(
            "POST", "/deployments", body=request_body
        )
        assert resp.status_code == 201

    # Verify that the 'nr_deployments' expected deployments have been created
    resp = api_mgmt_deploy.with_auth(user.utoken).call("GET", "/deployments")
    assert resp.status_code == 200
    new_count = int(resp.headers["X-Total-Count"])
    assert new_count == count + nr_deployments

    return tenant


@pytest.fixture(scope="function")
def setup_deployments_enterprise_test(
    clean_mongo, existing_deployments=3, nr_devices=100, plan="enterprise"
):
    """
    Creates two tenants, with one user each, where each user has three deployments,
    and a hundred devices each.
    """

    uuidv4 = str(uuid.uuid4())
    tenant1 = create_tenant_test_setup(
        "some.user+" + uuidv4 + "@example.com", "test.mender.io-" + uuidv4, plan=plan
    )
    # Add a second tenant to make sure that the functionality does not interfere with other tenants
    uuidv4 = str(uuid.uuid4())
    tenant2 = create_tenant_test_setup(
        "some.user+" + uuidv4 + "@example.com", "test.mender.io-" + uuidv4, plan=plan
    )
    # Create 'existing_deployments' predefined deployments to act as noise for the server to handle
    # for both users
    return tenant1, tenant2


def setup_devices_and_management_st(nr_devices=100, deploy_to_group=None):
    """
    Sets up user creates authorized devices.
    """
    uuidv4 = str(uuid.uuid4())
    user = create_user("some.user+" + uuidv4 + "@example.com", "secretsecret")
    useradmm = ApiClient(useradm.URL_MGMT)
    devauthd = ApiClient(deviceauth.URL_DEVICES)
    devauthm = ApiClient(deviceauth.URL_MGMT)
    invm = ApiClient(inventory.URL_MGMT)
    # log in user
    r = useradmm.call("POST", useradm.URL_LOGIN, auth=(user.name, user.pwd))
    assert r.status_code == 200
    utoken = r.text
    # Upload a dummy artifact to the server
    upload_image("/tests/data/test-artifact.mender", utoken)
    # count existing devices
    r = invm.with_auth(utoken).call(
        "GET", inventory.URL_DEVICES, qs_params={"per_page": 1}
    )
    assert r.status_code == 200
    count = int(r.headers["X-Total-Count"])
    # prepare accepted devices
    devs = make_accepted_devices(devauthd, devauthm, utoken, "", nr_devices)
    # wait for devices to be provisioned
    time.sleep(3)
    if deploy_to_group:
        for device in devs[:-1]:
            r = invm.with_auth(utoken).call(
                "PUT",
                inventory.URL_DEVICE_GROUP.format(id=device.id),
                body={"group": deploy_to_group},
            )
            assert r.status_code == 204

    r = invm.with_auth(utoken).call(
        "GET", inventory.URL_DEVICES, qs_params={"per_page": 1}
    )
    assert r.status_code == 200
    new_count = int(r.headers["X-Total-Count"])
    assert new_count == count + nr_devices

    return user, utoken, devs


def setup_devices_and_management_mt(nr_devices=100, deploy_to_group=None):
    """
    Sets up user and tenant and creates authorized devices.
    """
    uuidv4 = str(uuid.uuid4())
    tenant = create_org(
        "test.mender.io-" + uuidv4,
        "some.user+" + uuidv4 + "@example.com",
        "correcthorse",
        plan="enterprise",
    )
    user = tenant.users[0]
    useradmm = ApiClient(useradm.URL_MGMT)
    devauthd = ApiClient(deviceauth.URL_DEVICES)
    devauthm = ApiClient(deviceauth.URL_MGMT)
    invm = ApiClient(inventory.URL_MGMT)
    # log in user
    r = useradmm.call("POST", useradm.URL_LOGIN, auth=(user.name, user.pwd))
    assert r.status_code == 200
    utoken = r.text
    # Upload a dummy artifact to the server
    upload_image("/tests/data/test-artifact.mender", utoken)
    # count existing devices
    r = invm.with_auth(utoken).call(
        "GET", inventory.URL_DEVICES, qs_params={"per_page": 1}
    )
    assert r.status_code == 200
    count = int(r.headers["X-Total-Count"])
    # prepare accepted devices
    devs = make_accepted_devices(
        devauthd, devauthm, utoken, tenant.tenant_token, nr_devices
    )
    # wait for devices to be provisioned
    time.sleep(3)
    if deploy_to_group:
        for device in devs[:-1]:
            r = invm.with_auth(utoken).call(
                "PUT",
                inventory.URL_DEVICE_GROUP.format(id=device.id),
                body={"group": deploy_to_group},
            )
            assert r.status_code == 204

    # Check that the number of devices were created
    r = invm.with_auth(utoken).call(
        "GET", inventory.URL_DEVICES, qs_params={"per_page": 1}
    )
    assert r.status_code == 200
    new_count = int(r.headers["X-Total-Count"])
    assert new_count == count + nr_devices

    return user, tenant, utoken, devs


def try_update(
    device, default_artifact_name="bugs-bunny", default_device_type="qemux86-64"
):
    """
    Try to make an update with a device
    :param devices:               list of devices
    :param expected_status_code:  expected status code
    :param default_artifact_name: default artifact name of the
                                  artifact used in the request

    NOTE: You can override the device type and artifact name
          by creating a device_type/artifact_name member of the
          Device object.
    """
    api_dev_deploy = ApiClient(deployments.URL_DEVICES)
    # Try to retrieve next update and assert expected status code
    resp = api_dev_deploy.with_auth(device.token).call(
        "GET",
        deployments.URL_NEXT,
        qs_params={
            "artifact_name": getattr(device, "artifact_name", default_artifact_name),
            "device_type": getattr(device, "device_type", default_device_type),
        },
    )
    if resp.status_code == 200:
        # Update device status upon successful request
        api_dev_deploy.with_auth(device.token).call(
            "PUT",
            deployments.URL_STATUS.format(id=resp.json()["id"]),
            body={"status": "success"},
        )
    return resp.status_code


class _TestDeploymentsBase(object):
    def do_test_listing_deployments(self, clean_mongo, user_token, devs):
        api_dep_v2 = ApiClient(deployments_v2.URL_MGMT)
        api_mgmt_dep = ApiClient(deployments.URL_MGMT)

        # let's create deployments every 4s, so we can then try to use
        # created_before and created_after search filters
        creation_sleep_s = 4
        created_at_by_time = {}
        created_at_by_name = {}
        created_at_by_id = {}
        ids_by_name = {}
        name_prefix = "phaseddeployment"
        for i in ["1", "2", "3", "4", "5"]:
            deployment_req = {
                "name": name_prefix + i,
                "artifact_name": "deployments-phase-testing",
                "devices": [dev.id for dev in devs],
            }
            r = api_mgmt_dep.with_auth(user_token).call(
                "POST", deployments.URL_DEPLOYMENTS, deployment_req
            )
            created_at_by_time[time.time()] = i
            created_at_by_name[i] = int(time.time())
            deployment_id = os.path.basename(r.headers["Location"])
            assert len(deployment_id) != 0
            deployment_id = deployment_id.rsplit("/", 1)[-1]
            logger.info(f"created deployment: {deployment_id}")
            created_at_by_id[deployment_id] = created_at_by_name[i]
            ids_by_name[name_prefix + i] = deployment_id
            time.sleep(creation_sleep_s)

        resp = api_dep_v2.with_auth(user_token).call("GET", "/deployments")
        assert resp.status_code == 200
        assert len(resp.json()) == 5
        assert len(resp.json()) == int(resp.headers["X-Total-Count"])

        q = ""
        for i in ["1", "2", "3", "4", "5"]:
            q = q + "&name=" + name_prefix + i
        q = q.lstrip("&")

        # multiple names in the query
        resp = api_dep_v2.with_auth(user_token).call("GET", "/deployments" + "?" + q)
        assert resp.status_code == 200
        assert len(resp.json()) == 5
        assert len(resp.json()) == int(resp.headers["X-Total-Count"])

        q = ""
        for j in ["1", "2", "3", "4", "5"]:
            q = q + "&name=" + name_prefix + j
        q = q.lstrip("&")
        for page in ["1", "2", "3", "4", "5"]:
            resp = api_dep_v2.with_auth(user_token).call(
                "GET", "/deployments" + "?" + q + "&per_page=1&page=" + page
            )
            assert resp.status_code == 200
            assert len(resp.json()) == 1

        # created_after query parameter
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?created_after=" + str(created_at_by_name["2"])
        )
        assert resp.status_code == 200
        assert len(resp.json()) == 4
        assert len(resp.json()) == int(resp.headers["X-Total-Count"])

        # the deployments "2", "3", "4", "5" should be in the response
        for j in ["2", "3", "4", "5"]:
            existing_id = ids_by_name[name_prefix + j]
            found = False
            for d in resp.json():
                if d["id"] == existing_id:
                    found = True
                    break
            assert found

        # created_before query parameter
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?created_before=" + str(created_at_by_name["4"])
        )
        assert resp.status_code == 200
        assert len(resp.json()) == 3
        assert len(resp.json()) == int(resp.headers["X-Total-Count"])

        # per_page query parameter
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?per_page=1"
        )
        assert resp.status_code == 200
        assert len(resp.json()) == 1
        assert 5 == int(resp.headers["X-Total-Count"])

        # per_page and page query parameters
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?page=2&per_page=1"
        )
        assert resp.status_code == 200
        assert len(resp.json()) == 1
        assert 5 == int(resp.headers["X-Total-Count"])

        # per_page query parameter
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?per_page=1"
        )
        assert resp.status_code == 200
        assert len(resp.json()) == 1
        assert 5 == int(resp.headers["X-Total-Count"])

        # per_page query parameter
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?per_page=500"
        )
        assert resp.status_code == 200
        assert len(resp.json()) == 5
        assert 5 == int(resp.headers["X-Total-Count"])

        # pagination overshoot: page beyond last should return correct X-Total-Count
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?per_page=10&page=2"
        )
        assert resp.status_code == 200
        assert len(resp.json()) == 0
        assert int(resp.headers["X-Total-Count"]) == 5

        # per_page query parameter
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?per_page=18446744073709551617"
        )
        assert resp.status_code == 400

        # page query parameter
        resp = api_dep_v2.with_auth(user_token).call(
            "GET", "/deployments" + "?page=18446744073709551617"
        )
        assert resp.status_code == 400

    def do_test_regular_deployment(self, clean_mongo, user_token, devs):
        api_mgmt_dep = ApiClient(deployments.URL_MGMT)

        # Make deployment request
        deployment_req = {
            "name": "phased-deployment",
            "artifact_name": "deployments-phase-testing",
            "devices": [dev.id for dev in devs],
        }
        res = api_mgmt_dep.with_auth(user_token).call(
            "POST", deployments.URL_DEPLOYMENTS, deployment_req
        )
        assert res.status_code == 201
        depid = res.headers["Location"].split("/")[-1]
        stats = get_stats(depid, user_token)
        # after creating static deployment
        # all devices should be pending in stats
        verify_stats(stats, {"pending": len(devs)})

        for dev in devs:
            status_code = try_update(dev)
            assert status_code == 200
            dev.artifact_name = deployment_req["artifact_name"]

        # when running against staging, wait 5 seconds to avoid hitting
        # the rate limits for the devices (one inventory update / 5 seconds)
        isK8S() and time.sleep(5.0)

        for dev in devs:
            # Deployment already finished
            status_code = try_update(dev)
            assert status_code == 204

        # when running against staging, wait 5 seconds to avoid hitting
        # the rate limits for the devices (one inventory update / 5 seconds)
        isK8S() and time.sleep(5.0)

        deployment_req["name"] = "really-old-update"
        api_mgmt_dep.with_auth(user_token).call(
            "POST", deployments.URL_DEPLOYMENTS, deployment_req
        )
        for dev in devs:
            # Already installed
            status_code = try_update(dev)
            assert status_code == 204

    def do_test_regular_deployment_all_devices(self, clean_mongo, user_token, devs):
        api_mgmt_dep = ApiClient(deployments.URL_MGMT)

        # Make deployment request
        deployment_req = {
            "name": "phased-deployment",
            "artifact_name": "deployments-phase-testing",
            "all_devices": True,
        }
        res = api_mgmt_dep.with_auth(user_token).call(
            "POST", deployments.URL_DEPLOYMENTS, deployment_req
        )
        assert res.status_code == 201
        depid = res.headers["Location"].split("/")[-1]
        stats = get_stats(depid, user_token)
        # after creating static deployment for all devices,
        # all devices should be pending in stats
        verify_stats(stats, {"pending": len(devs)})

        for dev in devs:
            status_code = try_update(dev)
            assert status_code == 200
            dev.artifact_name = deployment_req["artifact_name"]

        # when running against staging, wait 5 seconds to avoid hitting
        # the rate limits for the devices (one inventory update / 5 seconds)
        isK8S() and time.sleep(5.0)

        for dev in devs:
            # Deployment already finished
            status_code = try_update(dev)
            assert status_code == 204

        # when running against staging, wait 5 seconds to avoid hitting
        # the rate limits for the devices (one inventory update / 5 seconds)
        isK8S() and time.sleep(5.0)

        deployment_req["name"] = "really-old-update"
        api_mgmt_dep.with_auth(user_token).call(
            "POST", deployments.URL_DEPLOYMENTS, deployment_req
        )
        for dev in devs:
            # Already installed
            status_code = try_update(dev)
            assert status_code == 204


@pytest.mark.storage_test
class TestDeploymentOpenSource(_TestDeploymentsBase):
    def test_regular_deployment(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st(5)
        self.do_test_regular_deployment(clean_mongo, user_token, devs)

    def test_listing_deployments(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st(5)
        self.do_test_listing_deployments(clean_mongo, user_token, devs)

    def test_regular_deployment_all_devices(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st(5)
        self.do_test_regular_deployment_all_devices(clean_mongo, user_token, devs)


# test status update


class StatusVerifier:
    def __init__(self, deploymentsm, deploymentsd):
        self.deploymentsm = deploymentsm
        self.deploymentsd = deploymentsd

    def status_update_and_verify(
        self,
        device_id,
        device_token,
        deployment_id,
        user_token,
        status_update,
        device_deployment_status,
        deployment_status,
        status_update_error_code=204,
        substate_update="",
        substate="",
    ):

        body = {"status": status_update}
        if substate_update != "":
            body = {"status": status_update, "substate": substate_update}

        # Update device status upon successful request
        resp = self.deploymentsd.with_auth(device_token).call(
            "PUT",
            deployments.URL_STATUS.format(id=deployment_id),
            body=body,
        )
        assert resp.status_code == status_update_error_code

        self.status_verify(
            deployment_id=deployment_id,
            user_token=user_token,
            device_id=device_id,
            device_deployment_status=device_deployment_status,
            deployment_status=deployment_status,
            substate=substate,
        )

    def status_verify(
        self,
        deployment_id,
        user_token,
        device_id="",
        device_deployment_status="",
        deployment_status="",
        substate="",
    ):

        if device_deployment_status != "":
            resp = self.deploymentsm.with_auth(user_token).call(
                "GET", deployments.URL_DEPLOYMENTS_DEVICES.format(id=deployment_id)
            )
            resp.status_code == 200

            devices = resp.json()

            for device in devices:
                if device["id"] == device_id:
                    assert device["status"] == device_deployment_status

                    if substate != "":
                        assert device["substate"] == substate

        if deployment_status != "":
            resp = self.deploymentsm.with_auth(user_token).call(
                "GET", deployments.URL_DEPLOYMENTS.format(id=deployment_id)
            )
            resp.status_code == 200
            assert resp.json()[0]["status"] == deployment_status


class _TestDeploymentsStatusUpdateBase:
    def do_test_deployment_status_update(
        self, clean_mongo, user_token, devs, deploy_to_group=None
    ):
        """
        deployment with four devices
        requires five devices (last one won't be part of the deployment
        """
        deploymentsm = ApiClient(deployments.URL_MGMT)
        deploymentsd = ApiClient(deployments.URL_DEVICES)

        status_verifier = StatusVerifier(deploymentsm, deploymentsd)

        # Make deployment request
        deployment_req = {
            "name": "phased-deployment",
            "artifact_name": "deployments-phase-testing",
            "devices": [dev.id for dev in devs[:-1]],
        }
        if deploy_to_group:
            deployment_req = {
                "name": "phased-deployment",
                "artifact_name": "deployments-phase-testing",
                "devices": [],
                "group": deploy_to_group,
                "retries": 0,
            }

        resp = deploymentsm.with_auth(user_token).call(
            "POST", deployments.URL_DEPLOYMENTS, deployment_req
        )
        if deploy_to_group:
            resp = deploymentsm.with_auth(user_token).call(
                "POST",
                deployments.URL_DEPLOYMENTS + "/group/" + deploy_to_group,
                deployment_req,
            )

        assert resp.status_code == 201

        # Store the location from the GET /deployments/{id} request
        deployment_id = os.path.basename(resp.headers["Location"])

        # Verify that the deployment is in "pending" state
        status_verifier.status_verify(
            deployment_id=deployment_id,
            user_token=user_token,
            deployment_status="pending",
        )

        default_artifact_name = "bugs-bunny"
        default_device_type = "qemux86-64"

        # Try to retrieve next update and assert expected status code
        resp = deploymentsd.with_auth(devs[0].token).call(
            "GET",
            deployments.URL_NEXT,
            qs_params={
                "artifact_name": getattr(
                    devs[0], "artifact_name", default_artifact_name
                ),
                "device_type": getattr(devs[0], "device_type", default_device_type),
            },
        )
        assert resp.status_code == 200

        # Try to retrieve next update for the device that already has the artifact
        resp = deploymentsd.with_auth(devs[1].token).call(
            "GET",
            deployments.URL_NEXT,
            qs_params={
                "artifact_name": "deployments-phase-testing",
                "device_type": getattr(devs[1], "device_type", default_device_type),
            },
        )
        assert resp.status_code == 204
        status_verifier.status_verify(
            deployment_id=deployment_id,
            user_token=user_token,
            device_id=devs[1].id,
            device_deployment_status="already-installed",
            deployment_status="inprogress",
        )

        # Try to retrieve next update for the device with incompatible device type
        resp = deploymentsd.with_auth(devs[2].token).call(
            "GET",
            deployments.URL_NEXT,
            qs_params={
                "artifact_name": getattr(
                    devs[2], "artifact_name", default_artifact_name
                ),
                "device_type": "foo",
            },
        )
        assert resp.status_code == 204
        status_verifier.status_verify(
            deployment_id=deployment_id,
            user_token=user_token,
            device_id=devs[2].id,
            device_deployment_status="noartifact",
            deployment_status="inprogress",
        )

        # device not part of the deployment
        status_verifier.status_update_and_verify(
            device_id=devs[4].id,
            device_token=devs[4].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="installing",
            device_deployment_status="does-not-matter",
            deployment_status="inprogress",
            status_update_error_code=404,
        )

        # wrong status
        status_verifier.status_update_and_verify(
            device_id=devs[0].id,
            device_token=devs[0].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="foo",
            device_deployment_status="pending",
            deployment_status="inprogress",
            status_update_error_code=400,
        )
        # device deployment: pending -> downloading
        status_verifier.status_update_and_verify(
            device_id=devs[0].id,
            device_token=devs[0].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="downloading",
            device_deployment_status="downloading",
            deployment_status="inprogress",
        )
        # devs[0] deployment: downloading -> installing
        # substate: "" -> "foo"
        status_verifier.status_update_and_verify(
            device_id=devs[0].id,
            device_token=devs[0].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="installing",
            device_deployment_status="installing",
            deployment_status="inprogress",
            substate_update="foo",
            substate="foo",
        )
        # devs[0] deployment: installing -> downloading
        """
        note that until the device deployment is finished
        transition to any of valid statuses is correct
        """
        status_verifier.status_update_and_verify(
            device_id=devs[0].id,
            device_token=devs[0].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="downloading",
            device_deployment_status="downloading",
            deployment_status="inprogress",
            substate="foo",
        )
        # devs[0] deployment: downloading -> rebooting
        # substate: "foo" -> "bar"
        status_verifier.status_update_and_verify(
            device_id=devs[0].id,
            device_token=devs[0].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="rebooting",
            device_deployment_status="rebooting",
            deployment_status="inprogress",
            substate_update="bar",
            substate="bar",
        )
        # devs[0] deployment: rebooting -> success
        status_verifier.status_update_and_verify(
            device_id=devs[0].id,
            device_token=devs[0].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="success",
            device_deployment_status="success",
            deployment_status="inprogress",
            substate="bar",
        )
        # devs[0] deployment already finished
        status_verifier.status_update_and_verify(
            device_id=devs[0].id,
            device_token=devs[0].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="pending",
            device_deployment_status="success",
            deployment_status="inprogress",
            status_update_error_code=400,
            substate="bar",
        )

        # Try to retrieve next update and assert expected status code
        resp = deploymentsd.with_auth(devs[3].token).call(
            "GET",
            deployments.URL_NEXT,
            qs_params={
                "artifact_name": getattr(
                    devs[3], "artifact_name", default_artifact_name
                ),
                "device_type": getattr(devs[3], "device_type", default_device_type),
            },
        )
        assert resp.status_code == 200

        # device deployment: pending -> failure
        # deployment: inprogress -> finished
        status_verifier.status_update_and_verify(
            device_id=devs[3].id,
            device_token=devs[3].token,
            deployment_id=deployment_id,
            user_token=user_token,
            status_update="failure",
            device_deployment_status="failure",
            deployment_status="finished",
        )


@pytest.mark.storage_test
class TestDeploymentsStatusUpdate(_TestDeploymentsStatusUpdateBase):
    def test_deployment_status_update(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st(5)
        self.do_test_deployment_status_update(clean_mongo, user_token, devs)


@pytest.mark.storage_test
class TestDeploymentsToGroupStatusUpdate(_TestDeploymentsStatusUpdateBase):
    def test_deployment_status_update(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st(
            5, deploy_to_group="g0"
        )
        self.do_test_deployment_status_update(
            clean_mongo, user_token, devs, deploy_to_group="g0"
        )


def create_tenant(name, username, plan):
    tenant = create_org(name, username, "correcthorse", plan=plan)
    user = tenant.users[0]
    r = ApiClient(useradm.URL_MGMT).call(
        "POST", useradm.URL_LOGIN, auth=(user.name, user.pwd)
    )
    assert r.status_code == 200
    user.utoken = r.text

    return tenant


@pytest.fixture(scope="function")
def setup_tenant(clean_mongo):
    uuidv4 = str(uuid.uuid4())
    tenant = create_tenant(
        "test.mender.io-" + uuidv4, "some.user+" + uuidv4 + "@example.com", "enterprise"
    )
    # give workflows time to finish provisioning
    time.sleep(10)
    return tenant


def predicate(attr, scope, t, val):
    return {"attribute": attr, "scope": scope, "type": t, "value": val}


def create_filter(name, predicates, utoken):
    f = {"name": name, "terms": predicates}

    r = (
        ApiClient(inventory_v2.URL_MGMT)
        .with_auth(utoken)
        .call("POST", inventory_v2.URL_FILTERS, f)
    )
    assert r.status_code == 201

    f["id"] = r.headers["Location"].split("/")[1]
    return f


def create_dynamic_deployment(
    name, predicates, utoken, max_devices=None, phases=None, status_code=201
):
    f = create_filter(name, predicates, utoken)

    api_dep_v2 = ApiClient(deployments_v2.URL_MGMT)

    depid = None
    with get_mender_artifact(name) as filename:

        upload_image(filename, utoken)

        deployment_req = {
            "name": name,
            "artifact_name": name,
            "filter_id": f["id"],
        }

        if max_devices is not None:
            deployment_req["max_devices"] = max_devices

        if phases is not None:
            deployment_req["phases"] = phases

        res = api_dep_v2.with_auth(utoken).call(
            "POST", deployments_v2.URL_DEPLOYMENTS, deployment_req
        )

        assert res.status_code == status_code
        if status_code != 201:
            return None

        depid = res.headers["Location"].split("/")[-1]

    newdep = get_deployment(depid, utoken)

    assert newdep["name"] == name
    assert newdep["filter"]["id"] == f["id"]
    assert newdep["filter"]["terms"] == predicates
    assert newdep["status"] == "pending"
    assert newdep["dynamic"]

    return newdep


def get_deployment(depid, utoken):
    api_dep_v1 = ApiClient(deployments.URL_MGMT)
    res = api_dep_v1.with_auth(utoken).call(
        "GET", deployments.URL_DEPLOYMENTS_ID, path_params={"id": depid}
    )
    assert res.status_code == 200
    return res.json()


def update_deployment_status(deployment_id, status, token):
    api_dev_deploy = ApiClient(deployments.URL_DEVICES)

    body = {"status": status}

    resp = api_dev_deploy.with_auth(token).call(
        "PUT",
        deployments.URL_STATUS.format(id=deployment_id),
        body=body,
    )
    assert resp.status_code == 204


def assert_get_next(code, dtoken, artifact_name=None):
    api_dev_deploy = ApiClient(deployments.URL_DEVICES)

    resp = api_dev_deploy.with_auth(dtoken).call(
        "GET",
        deployments.URL_NEXT,
        qs_params={"artifact_name": "dontcare", "device_type": "arm1"},
    )

    assert resp.status_code == code
    if code == 200:
        assert resp.json()["artifact"]["artifact_name"] == artifact_name


def set_status(depid, status, dtoken):
    api_dev_deploy = ApiClient(deployments.URL_DEVICES)

    res = api_dev_deploy.with_auth(dtoken).call(
        "PUT",
        deployments.URL_STATUS.format(id=depid),
        body={"status": status},
    )

    assert res.status_code == 204


def get_stats(depid, token):
    api_dev_deploy = ApiClient(deployments.URL_MGMT)

    res = api_dev_deploy.with_auth(token).call(
        "GET",
        deployments.URL_DEPLOYMENTS_STATISTICS.format(id=depid),
    )

    assert res.status_code == 200
    return res.json()


def verify_stats(stats, expected):
    for k, v in stats.items():
        if k in expected:
            assert stats[k] == expected[k]
        else:
            assert stats[k] == 0


class _TestDeploymentsArtifactBase(object):
    def do_test_show_artifact_size(self, clean_mongo, user_token, devs):
        api_mgmt_dep = ApiClient(deployments.URL_MGMT)

        # get artifact size
        r = api_mgmt_dep.with_auth(user_token).call(
            "GET",
            deployments.URL_DEPLOYMENTS_ARTIFACTS
            + "?name="
            + "deployments-phase-testing",
        )
        assert r.status_code == 200
        artifacts = r.json()
        assert len(artifacts) == 1
        artifact_size = artifacts[0]["size"]

        # Make deployment request
        deployment_req = {
            "name": "phased-deployment",
            "artifact_name": "deployments-phase-testing",
            "devices": [dev.id for dev in devs],
        }
        r = api_mgmt_dep.with_auth(user_token).call(
            "POST", deployments.URL_DEPLOYMENTS, deployment_req
        )
        assert r.status_code == 201
        depid = r.headers["Location"].split("/")[-1]

        # get deployment and check the total size is zero
        dep = get_deployment(depid, user_token)
        assert dep["statistics"]["total_size"] == 0

        # there is no artifact for the first device
        status_code = try_update(devs[0], default_device_type="foo")
        assert status_code == 204

        # second device has artifact already installed
        status_code = try_update(
            devs[1], default_artifact_name=deployment_req["artifact_name"]
        )
        assert status_code == 204

        # rest of the devices will perform normal update
        for dev in devs[2:]:
            status_code = try_update(dev)
            assert status_code == 200

        # get deployment again and check the total size
        # keep in mind that first first two devices shouldn't be counted
        dep = get_deployment(depid, user_token)
        assert dep["statistics"]["total_size"] == (len(devs) - 2) * artifact_size

        # verify device deployments
        r = api_mgmt_dep.with_auth(user_token).call(
            "GET", deployments.URL_DEVICE_DEPLOYMENTS_ID.format(id=devs[0].id)
        )
        assert r.status_code == 200
        dd = r.json()
        assert len(dd) == 1
        assert "image" not in dd[0]["device"]

        for dev in devs[1:]:
            r = api_mgmt_dep.with_auth(user_token).call(
                "GET", deployments.URL_DEVICE_DEPLOYMENTS_ID.format(id=dev.id)
            )
            assert r.status_code == 200
            dd = r.json()
            assert len(dd) == 1
            assert dd[0]["device"]["image"]["size"] == artifact_size

    def do_list_artifacts_v2(self, clean_mongo, user_token, test_artifacts):
        api_dep_v2 = ApiClient(deployments_v2.URL_MGMT)
        # we allready have the test-artifact.mender with name "deployments-phase-testing"

        # our artifacts list:
        #  ├─ deployments-phase-testing
        #  │   ├─ device_type: ["qemux86-64"]
        #  │   └─ description: "abc"
        #  ├─ test-artifact
        #  │   ├─ device_type: ["arm1"]
        #  │   └─ description: "abc"
        #  ├─ test-artifact-0
        #  │   ├─ device_type: ["arm1-0"]
        #  │   └─ description: "abc-0"
        #  └─ test-artifact-1
        #      ├─ device_type: ["arm1-1"]
        #      └─ description: "abc-1"

        # check that artifacts are created
        r = api_dep_v2.with_auth(user_token).call("GET", deployments_v2.URL_ARTIFACTS)
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(
            artifacts, [artifact["name"] for artifact in test_artifacts]
        )

        # search for single exact name
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?name=test-artifact"
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(artifacts, ["test-artifact"])

        # search for multiple exact names
        r = api_dep_v2.with_auth(user_token).call(
            "GET",
            deployments_v2.URL_ARTIFACTS + "?name=test-artifact&name=test-artifact-0",
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(artifacts, ["test-artifact", "test-artifact-0"])

        # search for names with prefix
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?name=test-artifact-*"
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(
            artifacts,
            [
                artifact["name"]
                for artifact in test_artifacts
                if artifact["name"].startswith("test-artifact-")
            ],
        )

        # search for names with prefix and exact name
        r = api_dep_v2.with_auth(user_token).call(
            "GET",
            deployments_v2.URL_ARTIFACTS
            + "?name=deployments-phase-testing&name=test-artifact-*",
        )
        assert r.status_code == 400

        # search for multiple prefix
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?name=test*&name=test-artifact-*"
        )
        assert r.status_code == 400

        # search for exact description
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?description=abc"
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(
            artifacts,
            [
                artifact["name"]
                for artifact in test_artifacts
                if artifact.get("description") == "abc"
            ],
        )

        # search for description with prefix
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?description=abc-*"
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(
            artifacts,
            [
                artifact["name"]
                for artifact in test_artifacts
                if artifact.get("description", "").startswith("abc-")
            ],
        )

        # search for exact device type
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?device_type=arm1"
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(
            artifacts,
            [
                artifact["name"]
                for artifact in test_artifacts
                if "arm1" in artifact.get("device_types")
            ],
        )

        # search for device type with prefix
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?device_type=arm1-*"
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(
            artifacts,
            [
                artifact["name"]
                for artifact in test_artifacts
                if any(
                    [typ.startswith("arm1-") for typ in artifact.get("device_types")]
                )
            ],
        )

        # test pagination
        r = api_dep_v2.with_auth(user_token).call(
            "GET", deployments_v2.URL_ARTIFACTS + "?page=2&per_page=3"
        )
        assert r.status_code == 200
        artifacts = r.json()
        compare_artifact_names(
            artifacts,
            [
                artifact["name"]
                for artifact in test_artifacts[3 : min(6, len(test_artifacts))]
            ],
        )


@pytest.mark.storage_test
class TestDeploymentArtifactOpenSource(_TestDeploymentsArtifactBase):
    def test_show_artifact_size(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st(10)
        self.do_test_show_artifact_size(clean_mongo, user_token, devs)

    def test_list_artifacts_v2(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st()
        artifacts = create_test_artifacts(utoken=user_token, mongo_client=clean_mongo)
        self.do_list_artifacts_v2(clean_mongo, user_token, artifacts)


class _TestReleasesBase:
    def do_test_delete_releases(self, clean_mongo, user_token, devs):
        api_mgmt_dep = ApiClient(deployments.URL_MGMT)
        api_mgmt_dep_v2 = ApiClient(deployments_v2.URL_MGMT)

        # Create a deployment that uses the release (artifact)
        deployment_req = {
            "name": "phased-deployment",
            "artifact_name": "deployments-phase-testing",
            "devices": [dev.id for dev in devs],
        }
        r = api_mgmt_dep.with_auth(user_token).call(
            "POST", deployments.URL_DEPLOYMENTS, deployment_req
        )
        assert r.status_code == 201

        # Check that we can't delete the release when
        # it's part of an active deployment
        r = api_mgmt_dep_v2.with_auth(user_token).call(
            "DELETE",
            deployments_v2.URL_RELEASES + "?name=" + "deployments-phase-testing",
        )
        assert r.status_code == 409

        # Perform the upgrade to mark the deployment as finished
        status_code = try_update(devs[0], default_device_type="foo")
        assert status_code == 204

        # Check that we can delete the release now that
        # the deployment is finished
        r = api_mgmt_dep_v2.with_auth(user_token).call(
            "DELETE",
            deployments_v2.URL_RELEASES + "?name=" + "deployments-phase-testing",
        )
        assert r.status_code == 204


@pytest.mark.storage_test
class TestReleasesOpenSource(_TestReleasesBase):
    def test_delete_releases(self, clean_mongo):
        _user, user_token, devs = setup_devices_and_management_st(1)
        self.do_test_delete_releases(clean_mongo, user_token, devs)
