#!/usr/bin/python3
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2016 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import os

import parent
from packagelib import *
from testlib import *


# candlepin on the services image has a lot of demo data preloaded
# useful info/commands:
#    Login: doc      password: password
#    org:   snowwhite
#
#    Login: admin    password: admin
#    org:   admin
#
# if you download product files onto the test machine, these will show up as installed products
# local directory: /etc/pki/product
# sample products used in the tests (different subscription results for doc and admin):
# /home/admin/candlepin/generated_certs/6050.pem
# /home/admin/candlepin/generated_certs/88888.pem
#
# to use the candlepin image on a test machine, either add the certificate or
# allow insecure connections (/etc/rhsm/rhsm.conf -> "insecure = 1")
#
# $IP is the ip of the candlepin machine
#
# add an activation key to a pool:
# curl --insecure --request POST --user admin:admin https://$IP:8443/candlepin/activation_keys/ff80808155ca50b10155ca50cd280010/pools/ff80808155ca50b10155ca51f04607e5
# register with: activation key "awesome_os_pool" and org "admin"
# or: subscription-manager register --activationkey awesome_os_pool --org admin --serverurl https://$IP:8443/candlepin
#
# in order to get the right ids for the activation key and pool, see ACTIVATION_KEY_COMMAND and POOL_COMMAND

WAIT_SCRIPT = """
set -x
for x in $(seq 1 200); do
    if curl -s https://%(addr)s:8443/candlepin; then
        break
    else
        sleep 1
    fi
done
"""

ACTIVATION_KEY_COMMAND = """curl -s --insecure --request GET --user admin:admin https://localhost:8443/candlepin/activation_keys |
    jq -r '.[] | select(.name == "awesome_os_pool") | select(.owner.displayName == "Admin Owner") | .id' """

POOL_COMMAND = """curl -s --insecure --request GET --user admin:admin https://localhost:8443/candlepin/pools |
    jq -r '.[] | select(.owner.key == "admin") | select(.contractNumber == "0") | select(.providedProducts[0].productId == "88888") | .id' """

class SubscriptionsCase(MachineCase):
    provision = {
        "0": {"address": "10.111.112.1/20"},
        "services": {"image": "services"}
    }

    def setUp(self):
        super(SubscriptionsCase, self).setUp()
        self.candlepin = self.machines['services']

        # wait for candlepin to be active and verify
        self.candlepin.execute("/root/run-candlepin")

        # download product info from the candlepin machine
        product_file_one = os.path.join(self.tmpdir, "6050.pem")
        product_file_two = os.path.join(self.tmpdir, "88888.pem")
        self.candlepin.download("/home/admin/candlepin/generated_certs/6050.pem", product_file_one)
        self.candlepin.download("/home/admin/candlepin/generated_certs/88888.pem", product_file_two)

        # upload product info to the test machine
        m = self.machine
        m.upload([product_file_one, product_file_two], "/etc/pki/product")

        # Install Candlepin CA cert to where curl,
        # subscription-manager, and insights-client can find it.
        candlepin_ca_cert = self.candlepin.execute("podman exec candlepin cat /etc/candlepin/certs/candlepin-ca.crt")
        m.write("/tmp/candlepin-ca.crt", candlepin_ca_cert)
        m.execute("cat /tmp/candlepin-ca.crt >>/etc/rhsm/ca/redhat-uep.pem")
        m.execute("cat /tmp/candlepin-ca.crt >>/etc/pki/tls/certs/ca-bundle.crt")

        # We also need to use the right hostname to match the certificate.
        m.execute("echo '10.111.112.100 services.cockpit.lan' >>/etc/hosts")

        # Apply some extra cleanups on rhel-atomic.  These cleanups
        # are necessary because all changes to /etc after a "atomic
        # host upgrade" and before the next boot are lost.
        if m.image == "rhel-atomic":
            m.execute("rm -f /etc/pki/consumer/* /etc/pki/entitlement/*")

        # Wait for the web service to be accessible
        args = {"addr": "services.cockpit.lan"}
        m.execute(WAIT_SCRIPT % args, timeout=360)

        if m.image != "rhel-atomic":  # insights-client not installed there
            m.write("/etc/insights-client/insights-client.conf",
"""
[insights-client]
gpg=False
auto_config=False
base_url=localhost:8888/r/insights
# HACK: alice.cert is not trusted by insights, and making it so is really difficult
cert_verify=False
username=admin
password=foobar
""")

            # insights-client to access it. That requires a good enough
            # TLS mock insights server certificate
            m.upload(["verify/files/mock-insights", "verify/files/alice.key", "verify/files/alice.pem"], "/var/tmp")
            m.spawn("/var/tmp/mock-insights", "mock-insights")

@skipImage("No subscriptions", "centos-7", "continuous-atomic", "debian-stable", "debian-testing", "fedora-29",
           "fedora-30", "fedora-i386", "fedora-atomic", "fedora-testing", "ubuntu-1804", "ubuntu-stable")
@skipImage("cockpit-subscriptions obsoleted by external subscription-manager-cockpit", "rhel-8-0", "rhel-8-0-distropkg", "rhel-8-1")
class TestSubscriptions(SubscriptionsCase):

    def testRegister(self):
        b = self.browser

        self.login_and_go("/subscriptions")

        register_button_sel = "label:contains('Status') + button:contains('Register')"
        unregister_button_sel = "label:contains('Status') + button:contains('Unregister')"

        # wait until we can open the registration dialog
        b.click(register_button_sel)

        b.wait_visible("#subscription-register-url")

        # optionally: check custom url
        if self.machine.image == 'rhel-7-6-distropkg':
            b.click("#subscription-register-url button")
            b.wait_visible("#subscription-register-url .dropdown-menu")
            custom_url_selector = "#subscription-register-url a:contains('Custom')"
            b.click(custom_url_selector)
            b.wait_not_present("#subscription-register-url.open")
        else:
            b.set_val("#subscription-register-url", "custom")

        # enter server and incorrect login data
        b.set_input_text("#subscription-register-url-custom", "services.cockpit.lan:8443/candlepin")
        b.set_input_text("#subscription-register-username", "doc")
        b.set_input_text("#subscription-register-password", "wrongpass")

        # try to register
        dialog_register_button_sel = "div.modal-footer .btn-primary"
        b.click(dialog_register_button_sel)

        # wait for message that we used wrong credentials
        b.wait_in_text("body", "Invalid credentials")

        # enter correct login data and try again, old error should disappear
        b.set_input_text("#subscription-register-password", "password")
        b.click(dialog_register_button_sel)

        b.wait_not_in_text("body", "Invalid credentials")

        # wait for message that we need to specify our org
        b.wait_in_text("body", "'Organization' required to register.")

        # now specify the org
        b.set_input_text("#subscription-register-org", "admin")

        # use an activation key
        activation_key_id = self.candlepin.execute(ACTIVATION_KEY_COMMAND).strip()
        pool_id = self.candlepin.execute(POOL_COMMAND).splitlines()[0].strip()
        key_url = "https://localhost:8443/candlepin/activation_keys/{key}/pools/{pool}".format(
            key=activation_key_id, pool=pool_id)
        self.candlepin.execute("curl -s --insecure --request POST --user admin:admin {url}".format(url=key_url))
        b.set_input_text("#subscription-register-username", "")
        b.set_input_text("#subscription-register-password", "")
        b.set_input_text("#subscription-register-password", "")
        b.set_input_text("#subscription-register-key", "awesome_os_pool")

        # try to register again
        b.click(dialog_register_button_sel)

        # old error should disappear
        b.wait_not_in_text("body", "'Organization' required to register.")

        # dialog should disappear
        b.wait_not_present(dialog_register_button_sel)

        # make sure this product isn't subscribed
        product_selector = "tr.listing-ct-item:contains('Snowy OS Premium Architecture Bits')"
        # expand
        b.click(product_selector)
        details_selector = "tr.listing-ct-panel:contains('Snowy OS Premium Architecture Bits')"
        b.wait_in_text(details_selector, "6050")
        b.wait_in_text(details_selector, "Not Subscribed")
        # collapse again
        b.click(product_selector)

        # find another one that is subscribed
        product_selector = "tr.listing-ct-item:contains('Shared File System Bits')"
        # expand
        b.click(product_selector)
        details_selector = "tr.listing-ct-panel:contains('Shared File System Bits')"
        b.wait_in_text(details_selector, "88888")
        b.wait_not_in_text(details_selector, "Not Subscribed")
        # collapse again
        b.click(product_selector)

        # unregister
        b.click(unregister_button_sel)

        # wait until we can register again and open the dialog
        # wait until we can open the registration dialog
        b.click(register_button_sel)

        b.wait_visible("#subscription-register-url")

        # check custom url
        if self.machine.image == 'rhel-7-6-distropkg':
            b.click("#subscription-register-url button")
            b.wait_visible("#subscription-register-url .dropdown-menu")
            custom_url_selector = "#subscription-register-url a:contains('Custom')"
            b.click(custom_url_selector)
            b.wait_not_present("#subscription-register-url.open")
        else:
            b.set_val("#subscription-register-url", "custom")

        # enter server data
        b.set_input_text("#subscription-register-url-custom", "services.cockpit.lan:8443/candlepin")

        # make sure we have an activation key on the target machine
        self.candlepin.execute("curl -s --insecure --request POST --user admin:admin {url}".format(url=key_url))

        # enter key and org
        b.set_input_text("#subscription-register-key", "awesome_os_pool")
        b.set_input_text("#subscription-register-org", "admin")
        b.click(dialog_register_button_sel)

        # dialog should disappear
        b.wait_not_present(dialog_register_button_sel)

        # make sure this product isn't subscribed
        product_selector = "tr.listing-ct-item:contains('Snowy OS Premium Architecture Bits')"
        # expand
        b.click(product_selector)
        details_selector = "tr.listing-ct-panel:contains('Snowy OS Premium Architecture Bits')"
        b.wait_in_text(details_selector, "6050")
        b.wait_in_text(details_selector, "Not Subscribed")
        # collapse again
        b.click(product_selector)

        # now verify with an unprivileged user
        b.logout()
        self.machine.execute("useradd junior; echo junior:foobar | chpasswd")
        self.login_and_go("/subscriptions", user="junior")
        b.wait_in_text(".curtains-ct h1", "current user isn't allowed to access system subscription")
        self.allow_authorize_journal_messages()

    @skipImage("insights-client not installed",  "rhel-atomic")
    def testInsights(self):
        b = self.browser

        self.login_and_go("/subscriptions")

        b.click("label:contains('Status') + button:contains('Register')")
        b.wait_visible("#subscription-register-url")

        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", "services.cockpit.lan:8443/candlepin")
        b.set_input_text("#subscription-register-username", "admin")
        b.set_input_text("#subscription-register-password", "admin")
        b.set_input_text("#subscription-register-org", "admin")
        dialog_register_button_sel = "div.modal-footer .btn-primary"
        b.click(dialog_register_button_sel)
        b.wait_not_present(dialog_register_button_sel)

        b.switch_to_top()
        b.click("a[href='/system']")
        b.enter_page("/system")

        b.wait_text("#insights_text", "Not connected to Insights")

        b.switch_to_top()
        b.click("a[href='/subscriptions']")
        b.enter_page("/subscriptions")

        b.click("label:contains('Insights') a:contains('Not connected')")
        b.wait_present('.modal-body:contains("This system is not connected")')
        b.click('.modal-footer button.apply')
        with b.wait_timeout(360):
            b.wait_not_present('.modal-dialog')

        b.switch_to_top()
        b.click("a[href='/system']")
        b.enter_page("/system")

        b.wait_not_visible("#insights_text")

        b.switch_to_top()
        b.click("a[href='/subscriptions']")
        b.enter_page("/subscriptions")

        b.click("label:contains('Insights') a:contains('Connected to Insights')")
        b.wait_present('.modal-body:contains("Next Insights data upload")')
        b.wait_present('.modal-body:contains("Last Insights data upload")')
        b.wait_not_present('.modal-body:contains("unknown")')

        b.click("a:contains('Disconnect from Insights')")
        b.click("button:contains('Disconnect from Insights')")
        b.wait_not_present('.modal-dialog')

        b.wait_present("label:contains('Insights') a:contains('Not connected')")

    @skipImage("insights-client not installed",  "rhel-atomic")
    def testSubAndInAndFail(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/subscriptions")

        b.click("label:contains('Status') + button:contains('Register')")
        b.wait_visible("#subscription-register-url")

        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", "services.cockpit.lan:8443/candlepin")
        b.set_input_text("#subscription-register-username", "admin")
        b.set_input_text("#subscription-register-password", "admin")
        b.set_input_text("#subscription-register-org", "admin")
        b.set_checked("#subscription-insights", True)
        dialog_register_button_sel = "div.modal-footer .btn-primary"
        b.click(dialog_register_button_sel)
        with b.wait_timeout(360):
            b.wait_not_present(dialog_register_button_sel)

        b.wait_present("label:contains('Insights') a:contains('Connected to Insights')")

        # Break the next upload and expect the warning triangle to tell us about it
        m.execute("mv /etc/insights-client/machine-id /etc/insights-client/machine-id.lost")
        m.execute("systemctl start insights-client")

        b.wait_present("label:contains('Insights') i.pficon-warning-triangle-o")

        b.click("label:contains('Insights') a:contains('Connected to Insights')")
        b.wait_present('.modal-body:contains("The last Insights data upload has failed")')
        b.click("button.cancel")

        # Unbreak it and retry
        m.execute("mv /etc/insights-client/machine-id.lost /etc/insights-client/machine-id")
        m.execute("systemctl start insights-client; while systemctl --quiet is-active insights-client; do sleep 1; done",
                  timeout=360)

        b.wait_not_present("label:contains('Insights') i.pficon-warning-triangle-o")

        b.click("label:contains('Status') + button:contains('Unregister')")
        b.wait_not_present("label:contains('Insights')")
        m.execute("test -f /etc/insights-client/.unregistered")


@skipImage("No subscriptions", "centos-7", "continuous-atomic", "debian-stable", "debian-testing", "fedora-29",
           "fedora-30", "fedora-i386", "fedora-atomic", "fedora-testing", "ubuntu-1804", "ubuntu-stable")
@skipImage("cockpit-subscriptions obsoleted by external subscription-manager-cockpit", "rhel-8-0", "rhel-8-0-distropkg", "rhel-8-1")
class TestSubscriptionsPackages(SubscriptionsCase, PackageCase):

    @skipImage("insights-client not installable",  "rhel-atomic")
    def testMissingPackages(self):
        m = self.machine
        b = self.browser

        m.execute("pkcon remove -y insights-client")

        self.createPackage("insights-client", "999", "1")
        self.enableRepo()
        m.execute("pkcon refresh")

        self.login_and_go("/subscriptions")

        b.click("label:contains('Status') + button:contains('Register')")
        b.wait_visible("#subscription-register-url")

        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", "services.cockpit.lan:8443/candlepin")
        b.set_input_text("#subscription-register-username", "admin")
        b.set_input_text("#subscription-register-password", "admin")
        b.set_input_text("#subscription-register-org", "admin")
        b.set_checked("#subscription-insights", True)
        b.wait_present('.modal-body:contains("The insights-client package will be installed")')
        b.click('.modal-footer button.apply')
        with b.wait_timeout(360):
            b.wait_not_present(".modal-dialog")

        # This will not have worked because the insights-client binary
        # is not actually there.  Try again with the connection dialog.

        m.execute("test -f /stamp-insights-client-999-1")
        m.execute("pkcon remove -y insights-client")
        m.execute("pkcon refresh")

        b.click("label:contains('Insights') a:contains('Not connected')")
        b.wait_present('.modal-body:contains("This system is not connected")')
        b.wait_present('.modal-body:contains("The insights-client package will be installed")')
        b.click('.modal-footer button.apply')
        with b.wait_timeout(360):
            b.wait_present('.modal-footer:contains("not-found")')
        b.click('.modal-footer button.cancel')

        m.execute("test -f /stamp-insights-client-999-1")

if __name__ == '__main__':
    test_main()
