#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
# 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 time
import uuid

import packagelib
import testlib

# candlepin on the services image has a lot of demo data preloaded
# useful info/commands:
#    login:          huey
#    password:       password
#    organizations:  admin, snowwhite, donaldduck
#
#    login:          duey
#    password:       password
#    org:            donaldduck
#
# of those, only the 'donaldduck' organization is in SCA mode
#
# product certificates are installed onto the test machine to make it possible
# to use those products; few sample products are used in the tests, because of
# the different subscriptions for users, see the PRODUCT_* variables
#
# to use the candlepin image on a test machine, either add the certificate or
# allow insecure connections (/etc/rhsm/rhsm.conf -> "insecure = 1")

# fmt: off
CLIENT_ADDR = "10.111.112.1"
CANDLEPIN_ADDR = "10.111.112.100"
CANDLEPIN_HOSTNAME = "services.cockpit.lan"
CANDLEPIN_URL = f"https://{CANDLEPIN_HOSTNAME}:8443/candlepin"

PRODUCT_DONALDY = {
    "id": "7050",
    "name": "Donaldy OS Premium Architecture Bits"
}

PRODUCT_SHARED = {
    "id": "88888",
    "name": "Shared File System Bits (no content)"
}
# fmt: on


def machine_restorecon(machine, path, *args):
    cmd = ["restorecon", "-R", path] + list(args)
    return machine.execute(cmd)


class SubscriptionsCase(testlib.MachineCase):
    # fmt: off
    provision = {
        "0": {"address": CLIENT_ADDR + "/20"},
        "services": {"image": "services"}
    }
    # fmt: on

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

        # start candlepin in the service machine;
        # see https://github.com/cockpit-project/bots/pull/1768
        self.candlepin.execute(["/root/run-candlepin"])

        # make sure the cockpit machine can resolve the service machine hostname
        m.write("/etc/hosts", f"{CANDLEPIN_ADDR} {CANDLEPIN_HOSTNAME}\n", append=True)

        # download product info from the candlepin machine
        def download_product(product):
            prod_id = product["id"]
            filename = os.path.join(self.tmpdir, f"{prod_id}.pem")
            self.candlepin.download(f"/home/admin/candlepin/generated_certs/{prod_id}.pem", filename)
            m.upload([filename], "/etc/pki/product-default")

        m.execute(["mkdir", "-p", "/etc/pki/product-default"])
        download_product(PRODUCT_DONALDY)
        download_product(PRODUCT_SHARED)

        # download the candlepin CA certificate
        candlepin_ca_filename = "candlepin-ca.pem"
        candlepin_ca_tmpfile = os.path.join(self.tmpdir, candlepin_ca_filename)
        self.candlepin.download("/home/admin/candlepin/certs/candlepin-ca.crt", candlepin_ca_tmpfile)
        # make it available for the system, updating the system certificate store
        m.upload([candlepin_ca_tmpfile], f"/etc/pki/ca-trust/source/anchors/{candlepin_ca_filename}")
        machine_restorecon(self.machine, "/etc/pki")
        m.execute(["update-ca-trust", "extract"])
        # make it available for subscription-manager too
        m.upload([candlepin_ca_tmpfile], f"/etc/rhsm/ca/{candlepin_ca_filename}")
        machine_restorecon(self.machine, "/etc/rhsm/ca/")

        # Wait for the web service to be accessible, with an initial delay
        # to give more time to Candlepin to start
        time.sleep(10)
        m.execute(f"until curl --fail --silent --show-error {CANDLEPIN_URL}/status; do sleep 1; done")

        # Setup the repositories properly using the Candlepin RPM GPG key
        m.execute(
            [
                "curl",
                "-o",
                "/etc/pki/rpm-gpg/RPM-GPG-KEY-candlepin",
                f"http://{CANDLEPIN_HOSTNAME}:8080/RPM-GPG-KEY-candlepin",
            ]
        )
        machine_restorecon(self.machine, "/etc/pki/rpm-gpg/")
        m.execute(
            [
                "subscription-manager",
                "config",
                "--rhsm.baseurl",
                f"http://{CANDLEPIN_HOSTNAME}:8080",
            ]
        )

        hostname = m.execute(["hostname"]).rstrip()

        if m.image.startswith("rhel-"):
            m.write(
                "/etc/insights-client/insights-client.conf",
                f"""
[insights-client]
auto_config=False
auto_update=False
base_url={hostname}:8443/r/insights
cert_verify=/var/lib/insights/mock-certs/ca.crt
authmethod=CERT
""",
            )

        m.upload(["files/mock-insights"], "/var/tmp")
        m.spawn("env PYTHONUNBUFFERED=1 /var/tmp/mock-insights", "mock-insights.log")


class TestSubscriptions(SubscriptionsCase):
    def testRegister(self):
        b = self.browser

        self.login_and_go("/subscriptions")

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

        b.wait_visible(".pf-v6-c-page__main")
        b.assert_pixels(".pf-v6-c-page__main", "unregistered-view")

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

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

        # enter server and incorrect login data
        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", CANDLEPIN_URL)
        b.set_input_text("#subscription-register-username", "huey")
        b.set_input_text("#subscription-register-password", "wrongpass")

        # Do not try to connect to insights
        b.set_checked("#subscription-insights", False)

        # try to register
        dialog_register_button_sel = "footer .pf-m-primary"
        b.click(dialog_register_button_sel)

        # wait for message that we used wrong credentials
        self.allow_browser_errors("error registering")
        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", "User huey is member of more organizations, but no organization was selected")

        b.assert_pixels("#register_dialog", "register-modal-error")

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

        # try to register again
        b.click(dialog_register_button_sel)

        # old error should disappear
        with b.wait_timeout(60):
            b.wait_not_in_text(
                "body", "User huey is member of more organizations, but no organization was selected"
            )

        # dialog should disappear
        b.wait_not_present(dialog_register_button_sel)

        b.assert_pixels(".pf-v6-c-page__main", "registered-view")

        # unregister
        with b.wait_timeout(360):
            b.click(unregister_button_sel)

        b.wait_visible(register_button_sel)

    def testRegisterWithKey(self):
        b = self.browser

        self.login_and_go("/subscriptions")

        # wait until we can open the registration dialog
        register_button_sel = "button:contains('Register')"
        unregister_button_sel = "button:contains('Unregister')"
        b.click(register_button_sel)

        # enter server data
        b.wait_visible("#subscription-register-url")
        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", CANDLEPIN_URL)

        # Do not try to connect to insights
        b.set_checked("#subscription-insights", False)

        # select registration method "activation key"
        activation_key_checkbox = "#subscription-register-activation-key-method"
        b.click(activation_key_checkbox)

        b.set_input_text("#subscription-register-key", "awesome_os_pool")
        b.set_input_text("#subscription-register-org", "donaldduck")

        b.assert_pixels("#register_dialog", "register-modal-key")

        dialog_register_button_sel = "footer .pf-m-primary"
        b.click(dialog_register_button_sel)

        # dialog should disappear
        with b.wait_timeout(60):
            b.wait_not_present(dialog_register_button_sel)

        # unregister
        b.click(unregister_button_sel)

    def testUnpriv(self):
        self.machine.execute("useradd junior; echo junior:foobar | chpasswd")
        self.login_and_go("/subscriptions", user="junior")
        self.browser.wait_in_text(
            ".pf-v6-c-empty-state__body", "current user isn't allowed to access system subscription"
        )
        self.allow_journal_messages("junior is not in the sudoers file.  This incident will be reported.")

    @testlib.onlyImage("Insights support is specific to RHEL", "rhel-*")
    def testInsights(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/subscriptions")

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

        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", CANDLEPIN_URL)
        b.set_input_text("#subscription-register-username", "huey")
        b.set_input_text("#subscription-register-password", "password")
        b.set_input_text("#subscription-register-org", "donaldduck")

        # Do not try to connect to insights
        b.set_checked("#subscription-insights", False)

        dialog_register_button_sel = "footer .pf-m-primary"
        b.click(dialog_register_button_sel)
        with b.wait_timeout(60):
            b.wait_not_present(dialog_register_button_sel)

        b.click("button:contains('Not connected')")
        b.wait_visible('.pf-v6-c-modal-box__body:contains("This system is not connected")')

        b.assert_pixels(".pf-v6-c-modal-box", "insights-modal-not-connected")

        b.click("footer button.apply")
        with b.wait_timeout(600):
            b.wait_not_present(".pf-v6-c-modal-box")

        # test system purpose
        m.execute(
            ["subscription-manager", "syspurpose", "role", "--set", "Red Hat Enterprise Linux Workstation"]
        )
        m.execute(["subscription-manager", "syspurpose", "usage", "--set", "Development/Test"])
        m.execute(["subscription-manager", "syspurpose", "service-level", "--set", "Standard"])
        b.wait_in_text("#syspurpose", "Standard")
        b.wait_in_text("#syspurpose", "Development/Test")
        b.wait_in_text("#syspurpose", "Red Hat Enterprise Linux Workstation")

        b.assert_pixels(".pf-v6-c-page__main", "registered-insights-view")

        b.click("button:contains('Connected to Insights')")
        b.wait_visible('.pf-v6-c-modal-box__body:contains("Next Insights data upload")')
        b.wait_visible('.pf-v6-c-modal-box__body:contains("Last Insights data upload")')

        b.assert_pixels(
            ".pf-v6-c-modal-box",
            "insights-modal-connected",
            mock={
                "#insights-modal tbody tr:first-of-type td": "in 14 hours",
                "#insights-modal tbody tr:last-of-type td": "less than a minute ago",
            },
        )

        b.click(".pf-v6-c-expandable-section__toggle button.pf-m-link:contains('Disconnect from Insights')")

        b.assert_pixels(
            ".pf-v6-c-modal-box",
            "insights-modal-disconnect-button",
            mock={
                "#insights-modal tbody tr:first-of-type td": "in 14 hours",
                "#insights-modal tbody tr:last-of-type td": "less than a minute ago",
            },
        )

        b.click("button.pf-m-danger:contains('Disconnect from Insights')")
        b.wait_not_present(".pf-v6-c-modal-box")

        b.wait_visible("button:contains('Not connected')")

    @testlib.onlyImage("Insights support is specific to RHEL", "rhel-*")
    def testSubAndInAndFail(self):
        m = self.machine
        b = self.browser

        # HACK - https://bugzilla.redhat.com/show_bug.cgi?id=2062136
        #
        # We rely on insights-client.service to be working, let's not
        # get distracted by SELinux.  Denials will still be logged and
        # tracked as known issues even when not enforcing.
        #
        m.execute(["setenforce", "0"])

        self.login_and_go("/subscriptions")

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

        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", CANDLEPIN_URL)
        b.set_input_text("#subscription-register-username", "huey")
        b.set_input_text("#subscription-register-password", "password")
        b.set_input_text("#subscription-register-org", "donaldduck")
        b.set_checked("#subscription-insights", True)
        dialog_register_button_sel = "footer .pf-m-primary"
        b.click(dialog_register_button_sel)
        with b.wait_timeout(360):
            b.wait_not_present(dialog_register_button_sel)

        with b.wait_timeout(600):
            b.wait_visible("button:contains('Connected to Insights')")

        # Break the next upload and expect the warning triangle to tell us about it;
        # write over the original file so its SELinux attributes are preserved.
        m.execute(["cp", "/etc/insights-client/machine-id", "/etc/insights-client/machine-id.orig"])
        m.write("/etc/insights-client/machine-id", str(uuid.uuid4()))
        m.execute(["systemctl", "start", "insights-client"])

        with b.wait_timeout(60):
            b.wait_visible("#overview button.pf-m-link .pf-v6-c-icon .pf-m-warning")

        b.assert_pixels("#overview", "insights-upload-failed-warning")

        b.click("button:contains('Connected to Insights')")
        b.wait_visible('.pf-v6-c-modal-box__body:contains("The last Insights data upload has failed")')

        b.assert_pixels(
            ".pf-v6-c-modal-box",
            "insights-modal-warning",
            mock={
                "#insights-modal tbody tr:first-of-type td": "in 14 hours",
                "#insights-modal tbody tr:last-of-type td": "less than a minute ago",
            },
        )

        b.click("button.cancel")

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

        b.wait_not_present("#overview button.pf-m-link .pf-v6-c-icon .pf-m-warning")

        b.click("button:contains('Unregister')")
        with b.wait_timeout(60):
            b.wait_not_in_text("#overview", "Insights")
        m.execute(["test", "-f", "/etc/insights-client/.unregistered"])

    def testTranslations(self):
        b = self.browser

        self.login_and_go("/subscriptions")

        b.switch_to_top()
        b.open_session_menu()
        b.click("button.display-language-menu")
        b.wait_visible("#display-language-modal")
        b.click("#display-language-modal li[data-value=de-de] button")
        b.click("#display-language-modal footer button.pf-m-primary")
        b.wait_language("de-de")

        # Wait for the shell to be fully loaded
        b.enter_page("/subscriptions")

        # Check that the subscriptions page is translated.
        b.click("#overview button:contains('Registrieren')")
        b.wait_visible("#register_dialog")
        b.wait_in_text("#register_dialog .pf-v6-c-modal-box__header", "System registrieren")

        b.switch_to_top()
        b.open_session_menu()
        b.click("button.display-language-menu")
        b.wait_visible("#display-language-modal")
        b.click("#display-language-modal li[data-value=en-us] button")
        b.click("#display-language-modal footer button.pf-m-primary")
        b.wait_language("en-us")

        b.enter_page("/subscriptions")
        b.wait_visible("#overview button:contains('Register')")


class TestSubscriptionsPackages(SubscriptionsCase, packagelib.PackageCase):
    def testMissingPackages(self):
        m = self.machine
        b = self.browser

        if m.image.startswith("rhel-"):
            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("button:contains('Register')")
        b.wait_visible("#subscription-register-url")

        b.set_val("#subscription-register-url", "custom")
        b.set_input_text("#subscription-register-url-custom", CANDLEPIN_URL)
        b.set_input_text("#subscription-register-username", "huey")
        b.set_input_text("#subscription-register-password", "password")
        b.set_input_text("#subscription-register-org", "donaldduck")
        b.set_checked("#subscription-insights", True)
        b.wait_visible(
            '.pf-v6-c-form__group-control:contains("The insights-client package will be installed")'
        )
        b.click("footer button.apply")
        with b.wait_timeout(360):
            b.wait_not_present(".pf-v6-c-modal-box")

        # Connecting to Insights will not have worked because the
        # insights-client binary is not actually there.

        b.wait_in_text(".pf-v6-c-alert.pf-m-danger .pf-v6-c-alert__description", "not-found")
        # Error can be dismissed, after dismissing there is no other error
        # checks that error was displayed exactly once
        b.click(".pf-v6-c-alert.pf-m-danger .pf-v6-c-alert__action button")
        b.wait_not_present(".pf-v6-c-alert.pf-m-danger")

        # 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("button:contains('Not connected')")
        b.wait_visible('.pf-v6-c-modal-box__body:contains("This system is not connected")')
        b.wait_visible('.pf-v6-c-modal-box__body:contains("The insights-client package will be installed")')
        b.click("footer button.apply")
        with b.wait_timeout(360):
            b.wait_visible('.pf-v6-c-modal-box__body:contains("not-found")')
        b.click("footer button.cancel")

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


if __name__ == "__main__":
    testlib.test_main()
