#!/usr/bin/env python

from __future__ import print_function

import argparse
import sys
import odcs.client.odcs
import requests
import time
import shutil
import tempfile
import openidc_client


class TemporaryDirectory(object):
    """
    Context manager for tempfile.mkdtemp() so it's usable with "with"
    statement.
    """
    def __enter__(self):
        self.name = tempfile.mkdtemp()
        return self.name

    def __exit__(self, exc_type, exc_value, traceback):
        shutil.rmtree(self.name)


def get_oidc_token():
    if "stg" in odcs_api_url:
        id_provider = 'https://id.stg.fedoraproject.org/openidc/'
    else:
        id_provider = 'https://id.fedoraproject.org/openidc/'

    # Get the auth token using the OpenID client.
    oidc = openidc_client.OpenIDCClient(
        'odcs',
        id_provider,
        {'Token': 'Token', 'Authorization': 'Authorization'},
        'odcs-authorizer',
        'notsecret',
    )

    scopes = [
        'openid',
        'https://id.fedoraproject.org/scope/groups',
        'https://pagure.io/odcs/new-compose',
        'https://pagure.io/odcs/renew-compose',
        'https://pagure.io/odcs/delete-compose',
    ]
    try:
        token = oidc.get_token(scopes, new_token=True)
    except requests.exceptions.HTTPError as e:
        print(e.response.text)
        raise
    return token


def get_packages_in_repo(repo_url):
    """
    Uses "dnf" API to list all packages available in remote repository defined
    by `repo_url`.
    """
    try:
        import dnf
    except ImportError:
        raise ImportWarning("Can't import dnf. Check your OS platform.")

    base = dnf.Base()

    with TemporaryDirectory() as temp_cache_dir:
        conf = base.conf
        conf.cachedir = temp_cache_dir

        # get rid of everything to be sure it's a blank slate
        base.reset(repos=True, goal=True, sack=True)

        # add a new repo requires an id, a conf object, and a baseurl
        base.repos.add_new_repo('my_test', conf, baseurl=[repo_url])
        base.fill_sack(load_system_repo=False)

        # Return available packages.
        return [x.name for x in base.sack.query().available()]


def check_compose(compose, source_type, source, packages, flags, arches=None):
    """
    Checks that the compose defined by compose data `compose` is properly
    generated for given input values `source_type, `source`, `packages`
    and `flags`.

    In case of error, this method raises an exception.
    """

    # Try to get the result_repofile.
    r = requests.get(compose["result_repofile"])
    r.raise_for_status()

    assert compose["state_name"] == "done"

    arches = arches or ["x86_64"]
    for arch in arches:
        if source_type == "pulp":
            # For pulp compose, check that the content_sets are
            # in the resulting repo file.
            for content_set in source.split(" "):
                assert content_set in r.text

            # Check that "arches" are set properly.
            assert arch in compose["arches"]

            # Check that "sigkeys" are set properly.
            assert compose["sigkeys"] == "FD431D51"
        elif source_type == "tag":
            # For Koji tag, try to get list of packages in a repo.
            baseurl = compose["result_repo"] + "/" + arch + "/os"
            pkgs = get_packages_in_repo(baseurl)
            print("Packages in resulting repo (%s): %r" % (arch, pkgs))

            # Check that all requested packages are in a compose
            for pkg in packages:
                assert pkg in pkgs

            # In case of "no_deps" flag, no extra packages are allowed in
            # a compose. In case the flag is not there, there must be more
            # packages in a compose because of dependencies of a package.
            if "no_deps" in flags:
                assert len(pkgs) == len(packages)
            else:
                assert len(pkgs) > len(packages)
        elif source_type == "module":
            # TODO
            # For Koji tag, try to get list of packages in a repo.
            baseurl = compose["result_repo"] + "/" + arch + "/os"
            pkgs = get_packages_in_repo(baseurl)
            print("Packages in resulting repo: %r" % pkgs)
            assert len(pkgs) > 0


def check_new_compose(source_type, source, packages, flags,
                      sigkeys=None, arches=None, expected_state_reason=None):
    """
    Submits new compose and checks the result.
    """
    print("Submitting new compose request: %s %s, %r %r" % (
          source_type, source, packages, flags))

    try:
        compose = client.new_compose(
            source=source,
            source_type=source_type,
            packages=packages,
            flags=flags,
            sigkeys=sigkeys,
            arches=arches,
        )
    except requests.exceptions.HTTPError as e:
        assert expected_state_reason in e.response.json()["message"]
        print("OK (HTTPError expected)")
        print ("")
        return

    compose = client.wait_for_compose(compose["id"])
    if expected_state_reason:
        assert compose["state_name"] == "failed"
        assert expected_state_reason in compose["state_reason"]
    else:
        check_compose(compose, source_type, source, packages, flags, arches)

    print("OK")
    print("")
    return compose["id"]


def check_renew_compose(compose_id, source_type, source, packages,
                        flags, arches=None):
    """
    Renews the compose and checks the compose is renewed properly.
    """
    print("Renewing compose: %s" % compose_id)
    compose = client.renew_compose(compose_id)
    compose = client.wait_for_compose(compose["id"])
    check_compose(compose, source_type, source, packages, flags, arches)
    print("OK")
    print("")
    return compose["id"]


def check_delete_compose(compose_id):
    """
    Deletes the compose and checks it is deleted properly.
    """
    print("Deleting compose: %s" % compose_id)
    client.delete_compose(compose_id)
    for i in range(10):
        compose = client.get_compose(compose_id)
        if compose["state_name"] == "removed":
            break
        time.sleep(1)
    assert compose["state_name"] == "removed"
    print("OK")
    print("")


def check_redhat_deployment():
    """
    Checks the Red Hat internal ODCS deployment
    """
    # Check "tag".
    compose_id = check_new_compose(
        "tag", "cf-1.0-rhel-5", ["gofer-package"], ["no_deps"],
        arches=["x86_64", "ppc64"])
    check_delete_compose(compose_id)
    check_renew_compose(compose_id, "tag", "cf-1.0-rhel-5",
                        ["gofer-package"], ["no_deps"], ["x86_64", "ppc64"])

    # Check "tag" with "deps".
    check_new_compose("tag", "cf-1.0-rhel-5", ["gofer"], [])

    # Check "tag" with errors.
    check_new_compose(
        "tag", "cf-1.0-rhel-5", [], [],
        expected_state_reason="\"packages\" must be defined for \"tag\" source_type.")
    check_new_compose(
        "tag", "unknown-tag", ["gofer-package"], [],
        expected_state_reason="Compose run failed: No such entry in table tag: unknown-tag")

    # Check "pulp".
    check_new_compose("pulp", "rhel-7-server-rpms rhel-server-rhscl-7-rpms", [], [])

    # Check "module".
    compose_id = check_new_compose("module", "postgresql-10", [], ["no_deps"], [""])
    check_delete_compose(compose_id)
    check_new_compose("module", "postgresql:10", [], ["no_deps"], [""])

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Test ODCS deployment.")
    parser.add_argument("odcs_api_url",
                        help="URL of the ODCS instance to test")
    parser.add_argument("profile",
                        help="can be either \"redhat\" or \"fedora\"")
    parser.add_argument("--fast-check", action="store_true",
                        help="perform just a single compose check")
    args = parser.parse_args()

    odcs_api_url = args.odcs_api_url
    profile = args.profile
    fast_check = args.fast_check

    if profile == "redhat":
        token = None
        auth_mech = odcs.client.odcs.AuthMech.Kerberos
    elif profile == "fedora":
        token = get_oidc_token()
        auth_mech = odcs.client.odcs.AuthMech.OpenIDC
    else:
        print("Unknown profile")
        sys.exit(2)

    client = odcs.client.odcs.ODCS(
        odcs_api_url,
        auth_mech=auth_mech,
        openidc_token=token,
    )

    if profile == "redhat":
        if fast_check:
            compose_id = check_new_compose(
                "pulp", "rhel-7-server-rpms rhel-server-rhscl-7-rpms", [], [])
            sys.exit(0) if compose_id else sys.exit(1)

        check_redhat_deployment()
    else:
        if fast_check:
            print("Ignoring --fast-check option. Applicable only for redhat profile.")
        compose_id = check_new_compose(
            "module", "testmodule-master", [], ["no_deps"])
        sys.exit(0) if compose_id else sys.exit(1)
