#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2015 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 subprocess

import testlib

INSTALL_RPMS = [
    "empty-1-0.noarch",
    "chrony-0.1-2.noarch",
    "tzdata-99999-2.noarch",
]

REPO_LOCATION = "/var/local-repo"
CHECKOUT_LOCATION = "/var/local-tree"
RPM_LOCATION = "/usr/share/rpm"
KEY_ID = "95A8BA1754D0E95E2B3A98A7EE15015654780CBD"

TAB_TREE = 1
TAB_PACKAGES = 2
TAB_SIGNATURES = 3


def switch_tab(b, index, tab):
    row_sel = f"#available-deployments > tbody:nth-of-type({index})"
    b.wait_visible(row_sel)
    if not b.is_visible(f"{row_sel} .ct-listing-panel-tabs"):
        b.click(f"{row_sel} .pf-v6-c-table__toggle button")
        b.wait_visible(f"{row_sel} .ct-listing-panel-tabs")
    b.click(f"{row_sel} .pf-v6-c-tabs__item:nth-of-type({tab}) button")


def wait_deployment_details_prop(b, index, tab, prop, value):
    switch_tab(b, index, tab)
    b.wait_text(f"#available-deployments > tbody:nth-of-type({index}) {prop}", value)


def wait_deployment_prop(b, index, prop, value):
    deployment = f"#available-deployments > tbody:nth-of-type({index})"
    if prop == "Actions":
        if value == "":
            b.wait_text(f"{deployment} tr:nth-child(1) td:nth-child(6)", value)
        else:
            b.wait_text(f"{deployment} tr:nth-child(1) td:nth-child(6) button", value)
    else:
        b.wait_in_text(f"{deployment} td[data-label={prop}]", value)


def wait_packages(b, index, packages):
    switch_tab(b, index, TAB_PACKAGES)
    for group, pkgs in packages.items():
        for pkg in pkgs:
            b.wait_in_text(f"#available-deployments > tbody:nth-of-type({index}) .{group}", pkg)


def wait_not_packages(b, index, packages):
    switch_tab(b, index, TAB_PACKAGES)
    for pkg in packages:
        b.wait_not_in_text(f"#available-deployments > tbody:nth-of-type({index})", pkg)


def check_package_count(b, assertIn, index):
    switch_tab(b, index, TAB_PACKAGES)
    for group in ['adds', 'removes']:
        b.call_js_func("ph_count_check", f"#available-deployments > tbody:nth-of-type({index}) .{group} dd", 1)
    # 1 or 2, in some scenarios cockpit-ostree gets up/downgraded as well
    assertIn(b.call_js_func("ph_count", f"#available-deployments > tbody:nth-of-type({index}) .up dd"), [1, 2])
    assertIn(b.call_js_func("ph_count", f"#available-deployments > tbody:nth-of-type({index}) .down dd"), [1, 2])


def do_deployment_action(b, index, action):
    deployment_row = f"#available-deployments > tbody:nth-of-type({index})"
    if action in ["pin", "unpin", "delete"]:
        b.click(f"{deployment_row} #deployment-actions")
        b.click(f".pf-v6-c-menu [data-action={action}] button")
    else:
        wait_deployment_prop(b, index, "Actions", action)
        b.click(f"{deployment_row} tr:nth-child(1) td:nth-child(6) button")
        b.wait_visible("#confirm-modal")
        b.click("#confirm-modal button.pf-m-warning")
        b.wait_not_present("#confirm-modal")


def ensure_remote_http_port(m, remote="local"):
    remote_spec = m.execute(f"ostree remote show-url {remote} || true")
    if remote_spec.startswith("http"):
        parts = remote_spec.strip().split(":")
        port = parts[-1]
    else:
        if remote_spec:
            m.execute(["ostree", "remote", "delete", remote])
        m.execute(["ostree", "remote", "add", "--no-gpg-verify", remote, "http://127.0.0.1:12345"])
        try:
            m.execute(["rpm-ostree", "reload"])
        except subprocess.CalledProcessError:
            m.execute(["systemctl", "restart", "rpm-ostreed"])
        port = 12345

    return port


def start_trivial_httpd(m, remote="local", location=REPO_LOCATION):
    port = ensure_remote_http_port(m, remote)
    pid = m.spawn(f"podman run -v {location}:/usr/local/nginx/html:ro,z -p {port}:80 quay.io/jitesoft/nginx", "httpd")

    m.execute(["ostree", "summary", f"--repo={location}", "-u"])
    m.wait_for_cockpit_running(port=port)
    return pid


def stop_trivial_httpd(m, pid):
    if pid:
        m.execute(["kill", str(pid)])


def generate_new_commit(m, pkg_to_remove):
    # Make one change of each type to a new rpm tree
    branch = m.execute(f"ostree refs --repo={REPO_LOCATION}").strip()

    m.upload([f"files/{k}.rpm" for k in INSTALL_RPMS],
             "/home/admin/")

    # move /usr/etc to /etc, makes rpm installs easier
    rpm_etc = os.path.join(CHECKOUT_LOCATION, "etc")
    usr_etc = os.path.join(CHECKOUT_LOCATION, "usr", "etc")
    m.execute(f"mv {usr_etc} {rpm_etc}")

    # Remove a package
    rpm_args = [CHECKOUT_LOCATION, RPM_LOCATION, pkg_to_remove]
    m.execute("rpm -e --verbose --root {0} --dbpath {1} {2}".format(*rpm_args))

    # Install our dummy packages, dbonly
    rpm_args[-1] = ' '.join([f"{os.path.join('/home/admin', x)}.rpm"
                             for x in INSTALL_RPMS])
    m.execute("rpm -U --oldpackage --root {0} --dbpath {1} --justdb {2}".format(*rpm_args))

    # move /etc back to /usr/etc to
    m.execute(f"mv {rpm_etc} {usr_etc}")

    # Upload a signing key
    m.upload(["files/secring.gpg",
              "files/pubring.gpg"], "/root/")

    m.execute(f"ostree commit -s cockpit-tree2 --repo {REPO_LOCATION} -b {branch} "
              "--add-metadata-string version=cockpit-base.2 "
              "--add-metadata-string rpmostree.inputhash=foo "
              f"--tree=dir={CHECKOUT_LOCATION} --gpg-sign={KEY_ID} --gpg-homedir=/root/", timeout=600)
    m.execute(["ostree", "summary", f"--repo={REPO_LOCATION}", "-u"])


def get_name(self):
    return "fedora-coreos"


def do_card_action(browser, kebab, action):
    browser.click(f"{kebab}")
    browser.wait_visible("ul.pf-v6-c-menu__list")
    browser.click(f"[data-action={action}] button")


@testlib.skipImage("No OSTree repo on bootc", "*-bootc")
class OstreeRestartCase(testlib.MachineCase):

    def testOstree(self):
        b = self.browser
        m = self.machine

        # Delete local remote so we start clean, without a file based remote
        ensure_remote_http_port(m)

        remove_pkg = m.execute("rpm -qa | grep socat").strip()

        chrony = m.execute("rpm -qa | grep chrony").strip()
        tzdata = m.execute("rpm -qa | grep tzdata").strip()

        m.start_cockpit()
        b.login_and_go("/updates")
        b.enter_page("/updates")

        # Check current and rollback target
        wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 1, "Status", "Current")
        wait_deployment_prop(b, 1, "Actions", "")

        wait_deployment_details_prop(b, 1, TAB_TREE, ".os", get_name(self))
        wait_deployment_details_prop(b, 1, TAB_TREE, ".version", "cockpit-base.1")

        b.assert_pixels("#ostree-status", "status", wait_after_layout_change=True)
        b.assert_pixels("#ostree-source", "source")
        b.assert_pixels("#available-deployments > tbody:nth-of-type(1)", "deployment",
                        # unpredictable expander head
                        skip_layouts=["medium"],
                        ignore=[".timestamp",
                                # The columns change size dependent on the second deployment's name.
                                "td[data-label=Name]", "td[data-label=State]", "td[data-label='Time']"])

        wait_packages(b, 1, {"rpms-col1": [chrony], "rpms-col2": [tzdata]})
        wait_packages(b, 1, {"rpms-col2": [remove_pkg]})
        wait_not_packages(b, 1, INSTALL_RPMS)

        wait_deployment_details_prop(b, 1, TAB_SIGNATURES, ".no-signatures", "No signature available")

        # Require signatures
        m.execute("sed -i /gpg-verify/d /etc/ostree/remotes.d/local.conf")

        b.wait_not_in_text("#available-deployments > tbody:nth-of-type(2) td[data-label=Version]", "cockpit")
        wait_deployment_prop(b, 2, "Actions", "Roll back")
        wait_deployment_details_prop(b, 2, TAB_TREE, ".os", get_name(self))

        # Check for new commit, get error
        b.click("#check-for-updates-btn")
        b.wait_visible('#app .pf-v6-c-alert.pf-m-danger')
        b.wait_visible("#check-for-updates-btn:not(disabled)")

        # Serve repo
        server_pid = start_trivial_httpd(m)

        # Check for new commit
        b.click("#check-for-updates-btn")
        # FIXME: racing, button is disabled for a very short time
        # b.wait_visible("#check-for-updates-btn:disabled")
        b.wait_visible("#check-for-updates-btn:not(disabled)")

        # Generate new commit
        generate_new_commit(m, remove_pkg)

        # Check again not trusted
        b.click("#check-for-updates-btn")
        b.wait_visible("#check-for-updates-btn:not(disabled)")
        b.wait_in_text("#app .pf-m-danger", "Can't check signature: public key not found")

        m.upload(["files/publickey.asc"], "/root/")
        m.execute("ostree remote gpg-import local -k /root/publickey.asc")

        # Check again have update data
        b.click("#check-for-updates-btn")
        b.wait_in_text("#ostree-status li:first-child div > div:last-child", "Update available")
        wait_deployment_prop(b, 1, "Status", "New")
        wait_deployment_prop(b, 1, "Actions", "Update")
        wait_deployment_prop(b, 2, "Status", "Current")

        # Check update data
        wait_deployment_details_prop(b, 1, TAB_TREE, ".os", get_name(self))
        wait_deployment_details_prop(b, 1, TAB_TREE, ".version", "cockpit-base.2")

        wait_packages(b, 1, {"up": ["tzdata-99999-2.noarch"],
                             "down": ["chrony-0.1-2.noarch"],
                             "adds": ["empty-1-0.noarch"],
                             "removes": [remove_pkg],
                             })
        check_package_count(b, self.assertIn, 1)

        # Check signatures
        switch_tab(b, 1, TAB_SIGNATURES)
        sel = "#available-deployments > tbody:nth-of-type(1)"
        b.wait_in_text(sel, KEY_ID)
        b.wait_in_text(sel, "RSA")
        b.wait_in_text(sel, "Cockpit Tester <do-not-reply@cockpit-project.org>")
        b.wait_in_text(sel, "Good signature")
        b.wait_in_text(sel, "When")

        # Force an error
        stop_trivial_httpd(m, server_pid)
        wait_deployment_prop(b, 1, "Status", "New")
        do_deployment_action(b, 1, "Update")
        with b.wait_timeout(60):
            wait_deployment_prop(b, 1, "Status", "Failedview more...New")
        server_pid = start_trivial_httpd(m)

        # Apply update
        do_deployment_action(b, 1, "Update")
        wait_deployment_prop(b, 1, "Status", "Updating")

        b.switch_to_top()
        with b.wait_timeout(120):
            b.wait_visible(".curtains-ct")

        b.wait_in_text(".curtains-ct h1", "Disconnected")

        m.wait_reboot()
        m.start_cockpit()
        b.reload()
        b.login_and_go("/updates")

        b.wait_in_text("#ostree-status li:first-child div > div:last-child", "System is up to date")
        # After reboot, check commit
        wait_deployment_prop(b, 1, "Version", "cockpit-base.2")
        wait_deployment_prop(b, 1, "Status", "Current")
        wait_deployment_prop(b, 1, "Actions", "")
        wait_packages(b, 1, {"rpms-col1": [INSTALL_RPMS[0], INSTALL_RPMS[1]],
                             "rpms-col2": [INSTALL_RPMS[2]],
                             })
        wait_not_packages(b, 1, [remove_pkg])

        # Check signatures
        switch_tab(b, 1, TAB_SIGNATURES)
        sel = "#available-deployments > tbody:nth-of-type(1)"
        b.wait_in_text(sel, KEY_ID)
        b.wait_in_text(sel, "RSA")
        b.wait_in_text(sel, "Cockpit Tester <do-not-reply@cockpit-project.org>")
        b.wait_in_text(sel, "Good signature")
        b.wait_in_text(sel, "When")

        # Check rollback target
        wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 2, "Actions", "Roll back")
        wait_deployment_prop(b, 2, "Status", "")
        wait_deployment_details_prop(b, 2, TAB_TREE, ".os", get_name(self))

        wait_packages(b, 2, {"down": [tzdata],
                             "up": [chrony],
                             "removes": ["empty-1-0.noarch"],
                             "adds": [remove_pkg],
                             })
        check_package_count(b, self.assertIn, 2)

        # Rollback
        do_deployment_action(b, 2, "Roll back")
        wait_deployment_prop(b, 2, "Status", "Updating")

        b.switch_to_top()
        with b.wait_timeout(120):
            b.wait_visible(".curtains-ct")

        b.wait_in_text(".curtains-ct h1", "Disconnected")
        m.wait_reboot()
        m.start_cockpit()
        b.reload()
        b.login_and_go("/updates")

        wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 1, "Status", "Current")

        wait_deployment_prop(b, 2, "Version", "cockpit-base.2")
        wait_deployment_prop(b, 2, "Actions", "Roll back")
        wait_deployment_prop(b, 2, "Status", "")

        # Only two deployments
        b.wait_not_present("#available-deployments > tbody:nth-of-type(3)")

        self.allow_restart_journal_messages()

    def testRebase(self):
        m = self.machine
        b = self.browser

        start_trivial_httpd(m)
        branch = m.execute(f"ostree refs --repo={REPO_LOCATION}").strip()

        # Add a new branch to the default repo
        m.execute(["ostree", "commit", f"--repo={REPO_LOCATION}",
                   "-b", "znew-branch", f"--tree=ref={branch}",
                   "--add-metadata-string", "version=branch-version"], timeout=600)
        m.execute(["ostree", "summary", f"--repo={REPO_LOCATION}", "-u"])

        m.start_cockpit()
        b.login_and_go("/updates")
        b.enter_page("/updates")

        b.wait_in_text("#current-repository", "local")
        b.wait_in_text("#current-branch", branch)

        # open rebase modal
        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.wait_in_text("#change-repository .pf-v6-c-menu-toggle__text", "local")
        b.wait_in_text("#change-branch .pf-v6-c-menu-toggle__text", branch)

        # rebase to new branch
        b.click("#change-branch button.pf-v6-c-menu-toggle")
        b.wait_not_in_text(".pf-v6-c-menu li:first-child button", "error")
        b.wait_in_text(".pf-v6-c-menu li:last-of-type button", "znew-branch")
        b.call_js_func("ph_count_check", "#change-branch li", 2)
        b.click(".pf-v6-c-menu li:last-of-type button")
        b.click("#rebase-repository-modal button.pf-m-primary")
        b.wait_in_text("#current-repository", "local")
        b.wait_in_text("#current-branch", "znew-branch")
        wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 1, "Status", "Current")
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", f"local:{branch}")

        b.click("#check-for-updates-btn")

        wait_deployment_prop(b, 1, "Version", "branch-version")
        wait_deployment_prop(b, 1, "Status", "New")
        wait_deployment_prop(b, 1, "Actions", "Rebase")
        wait_deployment_details_prop(b, 1, TAB_TREE, ".os", get_name(self))
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", "local:znew-branch")

        # Apply update
        do_deployment_action(b, 1, "Rebase")
        wait_deployment_prop(b, 1, "Status", "Updating")

        b.switch_to_top()
        with b.wait_timeout(120):
            b.wait_visible(".curtains-ct")

        b.wait_in_text(".curtains-ct h1", "Disconnected")
        m.wait_reboot()
        m.start_cockpit()
        b.reload()
        b.login_and_go("/updates")

        # After reboot, check commit
        wait_deployment_prop(b, 1, "Version", "branch-version")
        wait_deployment_prop(b, 1, "Status", "Current")
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", "local:znew-branch")

        self.allow_restart_journal_messages()

    def testDeploymentManagement(self):
        m = self.machine
        b = self.browser

        remove_pkg = m.execute("rpm -qa | grep socat").strip()

        m.start_cockpit()
        b.login_and_go("/updates")
        b.enter_page("/updates")

        # Check current and rollback deployment
        wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 1, "Status", "Current")
        wait_deployment_prop(b, 1, "Actions", "")
        wait_deployment_prop(b, 2, "Status", "")
        wait_deployment_prop(b, 2, "Actions", "Roll back")

        # Overlay a package
        OVERLAY_RPM = "empty-1-0.noarch"
        m.upload([f"files/{OVERLAY_RPM}.rpm"], "/home/admin/")
        m.execute(f"rpm-ostree install -C /home/admin/{OVERLAY_RPM}.rpm")

        # Deployment with overlay package
        wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 1, "Actions", "")
        wait_deployment_prop(b, 1, "Status", "")
        # Current deployment
        wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 2, "Actions", "")
        wait_deployment_prop(b, 2, "Status", "Current")

        # Reboot to apply overlay package
        m.reboot()
        m.start_cockpit()
        b.reload()
        b.login_and_go("/updates")

        # Deployment with overlay package
        wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 1, "Actions", "")
        wait_deployment_prop(b, 1, "Status", "Current")
        # Previous deployment
        wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 2, "Actions", "Roll back")
        wait_deployment_prop(b, 2, "Status", "")

        wait_packages(b, 1, {"rpms-col1": [OVERLAY_RPM]})
        do_card_action(b, "#deployments-actions", "reset")
        b.wait_visible("#reset-modal")
        b.click("#remove-overlays-checkbox")
        b.click("#reset-modal button.pf-m-warning")
        with b.wait_timeout(60):
            b.wait_not_present("#reset-modal")

        wait_deployment_details_prop(b, 1, TAB_PACKAGES, ".removes", "Removalsempty-1-0.noarch")

        m.reboot()
        m.start_cockpit()
        b.reload()
        b.login_and_go("/updates")
        wait_not_packages(b, 1, [OVERLAY_RPM])
        b.click("#available-deployments > tbody:nth-of-type(1) .pf-v6-c-table__toggle button")

        # Generate new commit
        generate_new_commit(m, remove_pkg)

        b.click("#check-for-updates-btn")
        wait_deployment_prop(b, 1, "Version", "cockpit-base.2")
        wait_deployment_prop(b, 1, "Actions", "Update")
        wait_deployment_prop(b, 1, "Status", "New")
        b.call_js_func("ph_count_check", "#available-deployments tbody tr", 3)

        wait_deployment_prop(b, 1, "Version", "cockpit-base.2")
        wait_deployment_prop(b, 1, "Status", "New")
        wait_deployment_prop(b, 1, "Actions", "Update")

        # Pin rollback deployment
        do_deployment_action(b, 3, "pin")
        wait_deployment_prop(b, 3, "Status", "Pinned")
        # Kebab menu is not available for update
        b.wait_not_present("""#available-deployments > tbody:nth-of-type(1) tr:nth-child(1)
                           td:last-child button.pf-v6-c-menu-toggle""")

        # Use clean up to remove pending and rollback deployments
        do_card_action(b, "#deployments-actions", "clean-up")
        b.wait_visible("#cleanup-deployment-modal")
        b.click("#pending-deployment-checkbox")
        b.click("#rollback-deployment-checkbox")
        b.click("#cleanup-deployment-modal button.pf-m-primary")
        b.wait_not_present("#cleanup-deployment-modal")

        # Rollback deployment wasn't deleted because it's pinned
        b.call_js_func("ph_count_check", "#available-deployments tbody tr", 2)

        # Manually delete base.1 deployment
        wait_deployment_prop(b, 2, "Status", "Pinned")
        do_deployment_action(b, 2, "unpin")
        b.wait_not_present("#available-deployments .pf-v6-c-dropdown__menu")
        wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
        wait_deployment_prop(b, 2, "Status", "")
        wait_deployment_prop(b, 2, "Actions", "Roll back")
        do_deployment_action(b, 2, "delete")

        # only one available deployment
        b.call_js_func("ph_count_check", "#available-deployments tbody tr", 1)


class OstreeCase(testlib.MachineCase):
    @testlib.skipImage("No OSTree repo on bootc", "*-bootc")
    def testRemoteManagement(self):
        m = self.machine
        b = self.browser

        start_trivial_httpd(m)
        branch = m.execute(f"ostree refs --repo={REPO_LOCATION}").strip()

        m.start_cockpit()
        b.login_and_go("/updates")
        b.enter_page("/updates")

        b.wait_not_present('#ostree-status .pf-v6-u-warning-color-100')

        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.wait_in_text(".pf-v6-c-modal-box h1", "Rebase repository and branch")
        b.wait_in_text("#change-repository .pf-v6-c-menu-toggle__text", "local")
        # open the branches menu to see the entries
        b.wait_in_text("#change-branch div.pf-v6-c-form__group-control p", branch)
        b.click("#rebase-repository-modal button.pf-m-link")

        # Add new repository
        do_card_action(b, "#ostree-source-actions", "add-repository")
        b.wait_visible("#add-repository-modal")
        b.set_input_text("#add-repository-modal #new-remote-name", "zremote test")
        b.set_input_text("#add-repository-modal #new-remote-url", "http://localhost:12344")
        b.click("#add-repository-modal #new-gpg-verify")
        b.click("#add-repository-modal button.pf-m-primary")
        b.wait_in_text("#add-repository-modal .pf-v6-c-alert.pf-m-danger", "Invalid remote name")
        b.set_input_text("#add-repository-modal #new-remote-name", "zremote-test1")

        b.assert_pixels(".pf-v6-c-modal-box", "dialog-add-repository", wait_after_layout_change=True)
        b.click("#add-repository-modal button.pf-m-primary")
        b.wait_not_present("#add-repository-modal #new-remote-name")
        b.wait_not_present("#add-repository-modal .pf-m-primary")

        # Try to add repository with same name
        do_card_action(b, "#ostree-source-actions", "add-repository")
        b.wait_visible("#add-repository-modal")
        b.set_input_text("#add-repository-modal #new-remote-name", "zremote-test1")
        b.set_input_text("#add-repository-modal #new-remote-url", "http://localhost:12344")
        b.click("#add-repository-modal button.pf-m-primary")
        b.wait_in_text("#add-repository-modal .pf-v6-c-alert.pf-m-danger", 'already exists')
        b.click("#add-repository-modal button.pf-m-link")
        b.wait_not_present("#add-repository-modal #new-remote-name")
        b.wait_not_present("#add-repository-modal .pf-m-primary")

        # rebase to newly added remote
        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.click("#change-repository button.pf-v6-c-menu-toggle")
        b.wait_in_text(".pf-v6-c-menu #zremote-test1", "zremote-test1")
        b.click("#zremote-test1")
        b.wait_in_text("#change-branch .pf-v6-u-text-color-status-danger", "While fetching")
        b.click("#rebase-repository-modal button.pf-m-primary")
        b.wait_not_present("#rebase-repository-modal")

        b.wait_in_text("#current-repository", "zremote-test1")
        b.wait_in_text("#current-branch", branch)
        b.wait_in_text("#ostree-status li:last-child .pf-v6-c-content--p", "While fetching")

        # Config created
        self.assertEqual(m.execute("cat /etc/ostree/remotes.d/zremote-test1.conf").strip(),
                         '[remote "zremote-test1"]\nurl=http://localhost:12344\ngpg-verify=true')
        # No custom keyring
        self.assertFalse(m.execute("ls /sysroot/ostree/repo/zremote-test1.trustedkeys.gpg || true"))

        # Refresh goes back to default
        b.reload()
        b.enter_page("/updates")
        b.wait_in_text("#current-repository", "local")
        b.wait_in_text("#current-branch", branch)

        # Create a new remote with commits, just use the rpm dir
        zrepo = "/var/zrepo"
        m.execute(f"mkdir -p {zrepo}")
        m.execute("mkdir -p /tmp/rpm-data/usr/share")
        m.execute("cp -r /usr/share/rpm /tmp/rpm-data/usr/share/")
        m.execute(["ostree", "init", "--repo", zrepo, "--mode", "archive-z2"])
        m.execute(["ostree", "commit", f"--repo={zrepo}",
                   "-b", "zremote-branch1", "--orphan", "--tree=dir=/tmp/rpm-data",
                   "--add-metadata-string", "version=zremote-branch1.1"], timeout=600)
        m.execute(["ostree", "commit", f"--repo={zrepo}",
                   "-b", "zremote-branch2", "--orphan", "--tree=dir=/tmp/rpm-data",
                   "--add-metadata-string", "version=zremote-branch2.1"], timeout=600)
        start_trivial_httpd(m, remote="zremote-test1", location=zrepo)

        # Add a new branch to the default repo
        m.execute(["ostree", "commit", f"--repo={REPO_LOCATION}",
                   "-b", branch, f"--tree=ref={branch}",
                   "--add-metadata-string", "version=bad-version"], timeout=600)
        m.execute(["ostree", "summary", f"--repo={REPO_LOCATION}", "-u"])

        # Edit zremote-test1 repository
        do_card_action(b, "#ostree-source-actions", "edit-repository")
        b.wait_visible("#edit-repository-modal")
        b.click("#edit-repository-modal #select-repository button.pf-v6-c-menu-toggle")
        b.click(".pf-v6-c-menu #zremote-test1")
        b.wait_visible("#edit-repository-modal #edit-remote-url[value='http://localhost:12344']")
        b.wait_visible("#edit-repository-modal #gpg-verify:checked")
        b.wait_not_present("redit-repository-modal #gpg-data")
        b.click("#edit-repository-modal button.pf-m-secondary")
        b.wait_not_present("#edit-repository-modal button.pf-m-secondary")
        # set invalid key
        b.set_input_text("#edit-repository-modal #gpg-data", "bad")
        b.click("#edit-repository-modal button.pf-m-primary")
        b.wait_visible("#edit-repository-modal div.pf-m-danger")

        with open(os.path.join(testlib.TEST_DIR, "files", "publickey.asc"), 'r') as fp:
            gpg_data = fp.read()

        # set valid key
        b.set_val("#edit-repository-modal #gpg-data", gpg_data)
        b.wait_val("#edit-repository-modal #gpg-data", gpg_data)
        b.focus("#edit-repository-modal #gpg-data")
        b.key("Backspace")
        b.click("#edit-repository-modal button.pf-m-primary")
        b.wait_not_present("#edit-repository-modal")
        m.execute("ls /sysroot/ostree/repo/zremote-test1.trustedkeys.gpg")

        # disable gpg verification
        do_card_action(b, "#ostree-source-actions", "edit-repository")
        b.wait_visible("#edit-repository-modal")
        b.click("#edit-repository-modal #select-repository button.pf-v6-c-menu-toggle")
        b.click(".pf-v6-c-menu #zremote-test1")
        b.wait_visible("#edit-repository-modal #edit-remote-url[value='http://localhost:12344']")
        b.wait_visible("#edit-repository-modal #gpg-verify:checked")
        b.click("#edit-repository-modal #gpg-verify")
        b.click("#edit-repository-modal button.pf-m-primary")
        b.wait_not_present("#edit-repository-modal")

        # rebase to zremote-branch1
        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.click("#rebase-repository-modal #change-repository button.pf-v6-c-menu-toggle")
        b.click(".pf-v6-c-menu #zremote-test1")
        b.wait_in_text("#change-repository .pf-v6-c-menu-toggle__text", "zremote-test1")
        b.wait_in_text("#change-branch .pf-v6-c-menu-toggle__text", "zremote-branch1")
        b.click("#rebase-repository-modal #change-branch button.pf-v6-c-menu-toggle")
        b.wait_in_text(".pf-v6-c-menu li:nth-child(1) button", "zremote-branch1")
        b.wait_in_text(".pf-v6-c-menu li:nth-child(2) button", "zremote-branch2")
        b.call_js_func("ph_count_check", "#change-branch li", 2)
        b.click("#rebase-repository-modal button.pf-m-primary")

        self.assertEqual(m.execute("cat /etc/ostree/remotes.d/zremote-test1.conf").strip(),
                         '[remote "zremote-test1"]\nurl = http://localhost:12344\ngpg-verify = false')

        # Check updates display
        wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", f"local:{branch}")

        b.click("#check-for-updates-btn")

        wait_deployment_prop(b, 1, "Version", "zremote-branch1.1")
        wait_deployment_prop(b, 1, "Status", "New")
        wait_deployment_prop(b, 1, "Actions", "Rebase")
        wait_deployment_details_prop(b, 1, TAB_TREE, ".os", get_name(self))
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", "zremote-test1:zremote-branch1")

        wait_deployment_details_prop(b, 1, TAB_PACKAGES, ".same-packages",
                                     "This deployment contains the same packages as your currently booted system")

        # Switching back shows pulled
        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.click("#rebase-repository-modal #change-branch button.pf-v6-c-menu-toggle")
        b.wait_in_text(".pf-v6-c-menu li:first-child button", "zremote-branch1")
        b.click(".pf-v6-c-menu li:first-child button")
        wait_deployment_prop(b, 1, "Version", "zremote-branch1.1")

        # Refresh, back to local, pull in update
        b.reload()
        b.enter_page("/updates")
        b.wait_in_text("#current-branch", branch)
        b.click("#check-for-updates-btn")
        wait_deployment_prop(b, 1, "Version", "bad-version")
        wait_deployment_prop(b, 1, "Actions", "Update")

        # Switching to branch shows pulled
        b.wait_in_text("#current-repository", "local")
        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.wait_in_text("#rebase-repository-modal #change-repository .pf-v6-c-menu-toggle__text", "local")
        b.click("#rebase-repository-modal #change-repository button.pf-v6-c-menu-toggle")
        b.click(".pf-v6-c-menu #zremote-test1")
        b.wait_in_text("#rebase-repository-modal #change-repository .pf-v6-c-menu-toggle__text", "zremote-test1")
        b.wait_in_text("#rebase-repository-modal #change-branch .pf-v6-c-menu-toggle__text", "zremote-branch1")
        b.click("#rebase-repository-modal button.pf-m-primary")
        b.wait_not_present("#rebase-repository-modal")

        b.wait_in_text("#current-repository", "zremote-test1")
        b.wait_in_text("#current-branch", "zremote-branch1")
        wait_deployment_prop(b, 1, "Version", "zremote-branch1.1")
        wait_deployment_prop(b, 1, "Status", "New")

        # Remove zremote-test1 repository
        do_card_action(b, "#ostree-source-actions", "remove-repository")
        b.wait_visible("#remove-repository-modal")
        b.call_js_func("ph_count_check", "#remove-repository-modal .pf-v6-c-form__group-control input", 4)
        # Current repository checkbox is disabled
        b.wait_visible("#remove-repository-modal #zremote-test1 + label.pf-m-disabled")
        b.click("#remove-repository-modal button.pf-m-link")

        # Change back to local repository
        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.click("#rebase-repository-modal #change-repository button.pf-v6-c-menu-toggle")
        b.click(".pf-v6-c-menu #local")
        b.wait_in_text("#rebase-repository-modal #change-repository .pf-v6-c-menu-toggle__text", "local")
        b.wait_in_text("#rebase-repository-modal #change-branch", branch)
        b.click("#rebase-repository-modal button.pf-m-primary")
        b.wait_not_present("#rebase-repository-modal")

        # Remove zremote-test1 repository
        do_card_action(b, "#ostree-source-actions", "remove-repository")
        b.wait_visible("#remove-repository-modal")
        b.call_js_func("ph_count_check", "#remove-repository-modal .pf-v6-c-form__group-control input", 4)
        b.wait_in_text("#remove-repository-modal #zremote-test1 + label", "zremote-test1")

        # Remove button is disabled when nothing is selected
        b.wait_visible("#remove-repository-modal button.pf-m-danger[aria-disabled='true']")

        b.click("#remove-repository-modal #zremote-test1")
        b.click("#remove-repository-modal button.pf-m-danger:not([aria-disabled='true'])")
        b.wait_not_present("#remove-repository-modal")

        # Verify that the repository is gone
        do_card_action(b, "#ostree-source-actions", "rebase")
        b.wait_visible("#rebase-repository-modal")
        b.click("#rebase-repository-modal #change-repository button.pf-v6-c-menu-toggle")
        b.call_js_func("ph_count_check", "#rebase-repository-modal #change-repository .pf-v6-c-menu__list li", 3)
        b.wait_in_text(".pf-v6-c-menu button.pf-m-selected", "local")
        b.click("#rebase-repository-modal #change-repository button.pf-v6-c-menu-toggle")
        b.wait_in_text("#change-branch div.pf-v6-c-form__group-control p", branch)

        b.click("#rebase-repository-modal button.pf-m-primary")
        b.wait_not_present("#rebase-repository-modal")

        b.wait_in_text("#current-repository", "local")
        b.wait_in_text("#current-branch", branch)
        wait_deployment_prop(b, 1, "Version", "bad-version")
        wait_deployment_prop(b, 1, "Status", "New")
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", f"local:{branch}")

    @testlib.nondestructive
    def testPermission(self):
        b = self.browser

        self.login_and_go("/updates", superuser=False)
        b.wait_in_text(".pf-v6-c-empty-state__body", "Not authorized")
        self.assertIn("Reconnect", b.text(".pf-v6-c-empty-state button"))

        b.become_superuser()
        b.switch_to_frame("cockpit1:localhost/updates")
        b.wait_visible('#available-deployments')

    @testlib.skipImage("No OSTree repo on bootc", "*-bootc")
    def testPageStatus(self):
        m = self.machine
        b = self.browser

        # preloading works, no updates available
        m.start_cockpit()
        b.login_and_go("/system")
        b.wait_text("#page_status_notification_updates", "System is up to date")
        self.assertEqual(b.attr("#page_status_notification_updates svg", "data-pficon"), "check")
        # go to updates page
        b.click("#page_status_notification_updates a")
        b.enter_page("/updates")

        # now generate an update
        remove_pkg = m.execute("rpm -qa | grep socat").strip()
        generate_new_commit(m, remove_pkg)
        m.upload(["files/publickey.asc"], "/root/")
        m.execute("ostree remote gpg-import local -k /root/publickey.asc")

        # updates page sees the new version
        b.click("#check-for-updates-btn")

        wait_deployment_prop(b, 1, "Version", "cockpit-base.2")

        # overview page notices the new version as well
        b.go("/system")
        b.enter_page("/system")
        b.wait_in_text("#page_status_notification_updates", "Update available:")
        b.wait_in_text("#page_status_notification_updates", f"{get_name(self)} cockpit-base.2")

        # check with new session
        b.logout()
        b.login_and_go("/system")
        b.wait_in_text("#page_status_notification_updates", "Update available:")
        b.wait_in_text("#page_status_notification_updates", f"{get_name(self)} cockpit-base.2")


class OstreeOCICase(testlib.MachineCase):
    def start_oci_registry(self):
        self.machine.execute(
            "podman run -d --name ostree-registry --rm -p 5000:5000 "
            "-v /var/lib/cockpit-test-registry/:/var/lib/registry localhost/test-registry")

    def stop_oci_registry(self):
        self.machine.execute("podman rm -ft0 ostree-registry")

    def testBasic(self):
        b = self.browser
        m = self.machine

        if m.image.endswith("-bootc"):
            # -bootc images start with the official OS bootc image, from a local rebuild during image creation
            old_repo = "ostree-unverified-registry:localhost/bootc"
            old_branch = "latest"
            new_repo = "ostree-unverified-registry:localhost:5000/bootc"
            version = m.execute("bootc status --format json | jq -r '.status.booted.image.version'").strip()
        else:
            # -coreos images start with the local ostree repo deployment
            old_repo = "local"
            old_branch = m.execute(f"ostree refs --repo={REPO_LOCATION}").strip()
            new_repo = "ostree-unverified-registry:localhost:5000/ostree-oci"
            version = "cockpit-base.1"
        new_branch = "cockpit1"
        old_repo_branch = f"{old_repo}:{old_branch}"
        new_repo_branch = f"{new_repo}:{new_branch}"
        bash_ver = m.execute("rpm -q bash").strip()
        less_ver = m.execute("rpm -q less").strip()

        self.start_oci_registry()

        self.login_and_go("/updates")

        # our image defaults to local OSTree repo
        b.wait_text("#current-repository .pf-v6-c-description-list__description", old_repo)
        b.wait_text("#current-branch .pf-v6-c-description-list__description", old_branch)
        wait_deployment_prop(b, 1, "Version", version)
        wait_deployment_prop(b, 1, "Status", "Current")
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", f"{old_repo}:{old_branch}")
        wait_packages(b, 1, {"rpms-col1": [bash_ver]})

        # rebase to our new OCI repo
        # FIXME: there is currently no UI way of doing this
        if m.image.endswith("-bootc"):
            # this works with rpm-ostree as well, but let's use the future designated CLI
            m.execute(f"bootc switch localhost:5000/bootc:{new_branch}")
        else:
            m.execute(f"rpm-ostree rebase {new_repo_branch}")

        # UI picks this up
        wait_deployment_prop(b, 1, "Version", version)
        wait_deployment_prop(b, 1, "Status", "")
        wait_deployment_prop(b, 1, "Branch", new_repo_branch)
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", new_repo_branch)
        if m.image.endswith("-bootc"):
            # the new branch removes "less"
            wait_deployment_details_prop(b, 1, TAB_PACKAGES, ".removes", "Removals" + less_ver)
        else:
            # the repo is a straight rebuild of the OSTree, no package changes
            wait_deployment_details_prop(
                b, 1, TAB_PACKAGES, ".same-packages",
                "This deployment contains the same packages as your currently booted system")

        # current deployment is now second
        wait_deployment_prop(b, 2, "Version", version)
        wait_deployment_prop(b, 2, "Status", "Current")
        wait_deployment_details_prop(b, 2, TAB_TREE, ".origin", old_repo_branch)
        wait_packages(b, 2, {"rpms-col1": [bash_ver]})

        # podman sometimes leaves the container behind after reboot despite --rm
        self.stop_oci_registry()

        # reboot into new available image (not in UI, as it's already deployed)
        m.reboot()
        self.start_oci_registry()
        self.login_and_go("/updates")

        # OCI is now the current deployment
        b.wait_text("#current-repository .pf-v6-c-description-list__description", new_repo)
        b.wait_text("#current-branch .pf-v6-c-description-list__description", new_branch)

        # now the status is reversed: OCI deployment is running and has the package list
        wait_deployment_prop(b, 1, "Version", version)
        wait_deployment_prop(b, 1, "Status", "Current")
        wait_deployment_prop(b, 1, "Branch", new_repo_branch)
        wait_deployment_details_prop(b, 1, TAB_TREE, ".origin", new_repo_branch)
        wait_packages(b, 1, {"rpms-col1": [bash_ver]})
        # ... and second deployment is the ostree repo one
        wait_deployment_prop(b, 2, "Version", version)
        wait_deployment_prop(b, 2, "Status", "")
        wait_deployment_details_prop(b, 2, TAB_TREE, ".origin", old_repo_branch)
        if m.image.endswith("-bootc"):
            # the new branch removes "less", so the old one adds it
            wait_deployment_details_prop(b, 2, TAB_PACKAGES, ".adds", "Additions" + less_ver)
        else:
            # the repo is a straight rebuild of the OSTree, no package changes
            wait_deployment_details_prop(
                b, 2, TAB_PACKAGES, ".same-packages",
                "This deployment contains the same packages as your currently booted system")


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