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

#
# Copyright (C) 2017 Red Hat, Inc.
# SPDX-License-Identifier: LGPL-2.1-or-later

import json
import time

import packagelib
import testlib


@testlib.skipWsContainer("Not supported")
@testlib.skipImage("TODO: fix for OpenSUSE", "opensuse-tumbleweed")
@testlib.nondestructive
class TestApps(packagelib.PackageCase):

    def setUp(self) -> None:
        super().setUp()
        self.appstream_collection: 'set[str]' = set()
        self.machine.upload(["verify/files/test.png"], "/var/tmp/")

    def createAppStreamPackage(self, name: str, version: str, revision: str, install: bool = False) -> None:
        self.createPackage(name, version, revision, content={
            f"/usr/share/metainfo/org.cockpit_project.{name}.metainfo.xml": f"""
<component type="addon">
  <extends>org.cockpit_project.cockpit</extends>
  <id>org.cockpit-project.{name}</id>
  <icon type="stock">{name}</icon>
  <name>{name}</name>
  <summary>{name} application for testing</summary>
  <launchable type="cockpit-manifest">{name}</launchable>
</component>
""",
            f"/usr/share/icons/hicolor/64x64/apps/{name}.png": {"path": "/var/tmp/test.png"}}, install=install)
        self.appstream_collection.add(name)

    def createAppStreamRepoPackage(self, subdir: str | None = None) -> None:
        if subdir is None:
            subdir = "swcatalog/xml"
        body = ""
        for p in self.appstream_collection:
            body += f"""
  <component type="addon">
  <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.{p}</id>
    <icon type="cached">test.png</icon>
    <name>{p}</name>
    <summary>An application for testing</summary>
    <launchable type="cockpit-manifest">{p}</launchable>
    <description>
        <p>DESCRIPTION:none</p>
    </description>
    <url type="homepage">https://{p}.com</url>
    <pkgname>{p}</pkgname>
  </component>
"""
        self.machine.execute(f"mkdir -p /usr/share/{subdir}")
        self.createPackage("appstream-data-test", "1.0", "1", content={
            f"/usr/share/{subdir}/test.xml": f"""
<components origin="test">
{body}
</components>
            """,
            f"/usr/share/{subdir.split('/')[0]}/icons/test/64x64/test.png": {"path": "/var/tmp/test.png"}})
        self.enableRepo(refresh_repo=True)
        # ignore the corresponding journal entry
        self.allow_journal_messages("org.freedesktop.PackageKit.*org.freedesktop.DBus.Error.NoReply.*")

    def testBasic(self, urlroot: str = ""):
        b = self.browser
        m = self.machine

        self.allow_journal_messages("can't remove watch: Invalid argument")

        self.restore_dir("/usr/share/metainfo", reboot_safe=True)
        self.restore_dir("/usr/share/swcatalog", reboot_safe=True)
        self.restore_dir("/usr/share/app-info", reboot_safe=True)
        self.restore_dir("/var/cache/app-info", reboot_safe=True)

        # Make sure none of the appstream directories exist.  They
        # will be created later and we need to cope with that.
        m.execute("rm -rf /usr/share/metainfo /usr/share/swcatalog /usr/share/app-info /var/cache/app-info")

        # instead of the actual distro packages, use our own fake repo data package
        config = {"config": {"appstream_data_packages": ["appstream-data-test"]}}
        # Debian/Ubuntu have an appstream configuration package which makes apt update fetch appstream data
        if m.image.startswith("ubuntu") or m.image.startswith("debian"):
            self.createPackage("appstream-config", "1", "1")
            config["config"]["appstream_config_packages"] = ["appstream-config"]

        self.write_file("/etc/cockpit/apps.override.json", json.dumps(config))

        if urlroot != "":
            m.write("/etc/cockpit/cockpit.conf", f"[WebService]\nUrlRoot={urlroot}")

        self.login_and_go("/apps", urlroot=urlroot)
        b.wait_in_text(".pf-v6-c-empty-state", "No applications installed or available")
        b.wait_in_text(".pf-v6-c-empty-state", "Application information is missing")

        # still no metadata, but already installed application
        self.createAppStreamPackage("already", "1.0", "1", install=True)
        b.wait_not_present(".pf-v6-c-empty-state")
        b.wait_visible(".app-list .pf-v6-c-data-list__item-action[rowid='already'] button:contains('Remove')")
        b.wait_in_text(".pf-v6-c-alert", "Application information is missing")

        self.createAppStreamPackage("app-1", "1.0", "1")
        self.createAppStreamRepoPackage()

        # Install package metadata
        b.click(".pf-v6-c-alert button")

        with b.wait_timeout(60):
            b.wait_visible(".app-list #app-1")
            b.wait_not_present(".pf-v6-c-alert")
            b.click(".app-list #app-1")

        b.wait_visible('a[href="https://app-1.com"]')
        b.wait_visible(f'#app-page img[src^="{urlroot}/cockpit/channel/"]')
        b.click(".pf-v6-c-breadcrumb a:contains('Applications')")

        b.wait_visible("#list-page")
        b.wait_not_present("#app-page")

        if urlroot == "":
            b.assert_pixels("#list-page", "list-page")

        b.click(".app-list .pf-v6-c-data-list__item-action[rowid='app-1'] button:contains('Install')")
        b.wait_text(".app-list .pf-v6-c-data-list__item-action[rowid='app-1'] button", "Remove")
        b.wait_visible(f".app-list .pf-v6-c-data-list__item-row img[alt='icon of app-1'][src^='{urlroot}/cockpit/channel/']")
        m.execute("test -f /stamp-app-1-1.0-1")

        b.click(".app-list .pf-v6-c-data-list__item-action[rowid='app-1'] button:contains('Remove')")
        b.wait_text(".app-list .pf-v6-c-data-list__item-action[rowid='app-1'] button", "Install")
        b.wait_visible(f".app-list .pf-v6-c-data-list__item-row img[alt='icon of app-1'][src^='{urlroot}/cockpit/channel/']")
        m.execute("! test -f /stamp-app-1-1.0-1")

    def testWithUrlRoot(self):
        self.testBasic(urlroot="/webcon")

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

        self.allow_journal_messages("can't remove watch: Invalid argument")

        self.restore_dir("/usr/share/metainfo", reboot_safe=True)
        self.restore_dir("/usr/share/swcatalog", reboot_safe=True)
        self.restore_dir("/usr/share/app-info", reboot_safe=True)
        self.restore_dir("/var/cache/app-info", reboot_safe=True)

        # Make sure none of the appstream directories exist.  They
        # will be created later and we need to cope with that.
        m.execute("rm -rf /usr/share/metainfo /usr/share/swcatalog /usr/share/app-info /var/cache/app-info")

        # use a fake distro map
        self.write_file("/etc/cockpit/apps.override.json",
                        '{ "config": { "appstream_data_packages":'
                        '    {"testy": ["appstream-data-test"], "otheros": ["nosuchpackage"]},'
                        '              "appstream_config_packages": []'
                        '  }}')

        self.createAppStreamPackage("app-1", "1.0", "1")
        # old subdir until Fedora 39
        self.createAppStreamRepoPackage(subdir="app-info/xmls")

        self.login_and_go("/apps")
        b.wait_visible(".pf-v6-c-empty-state")

        # os-release is a symlink target, don't clobber that
        self.restore_file("/etc/os-release")
        m.execute("rm /etc/os-release")

        # get along with absent os-release
        b.click("#refresh")
        # the progress bar is too fast to reliably catch it
        time.sleep(1)
        b.wait_not_present("#refresh-progress")
        b.wait_visible(".pf-v6-c-empty-state")
        # wait until check for installed metadata package finished
        b.wait_attr("#list-page", "data-packages-checked", "true")
        # no appstream metadata available, don't advertise it
        b.wait_in_text(".pf-v6-c-empty-state", "No applications installed or available")
        self.assertNotIn("Install application information", b.text(".pf-v6-c-empty-state"))
        b.wait_not_present(".pf-v6-c-empty-state button")

        # unknown OS: nothing gets installed
        m.write("/etc/os-release", 'ID="unmapped"\nID_LIKE="mysterious"\nVERSION_ID="1"\n')
        b.click("#refresh")
        # the progress bar is too fast to reliably catch it
        time.sleep(1)
        b.wait_not_present("#refresh-progress")
        b.wait_visible(".pf-v6-c-empty-state")

        # known OS: appstream-data-test gets installed from the map
        m.write("/etc/os-release", 'ID="derivative"\nID_LIKE="spicy testy"\nVERSION_ID="1"\n')
        b.click("#refresh")
        m.execute("until test -e /usr/share/app-info/xmls/test.xml; do sleep 1; done")
        b.wait_visible(".app-list #app-1")

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

        # Switching to a language might produce these messages, which seem to be harmless.
        self.allow_journal_messages("invalid or unusable locale.*",
                                    "Error .* data: Connection reset by peer")

        # Reset everything
        m.execute("for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do mkdir -p $d; mount -t tmpfs tmpfs $d; done")
        self.addCleanup(m.execute, "for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do umount $d; done")

        self.login_and_go("/apps")
        b.wait_visible(".pf-v6-c-empty-state")

        m.write("/usr/share/app-info/xmls/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.foo</id>
    <name>NAME:none</name>
    <name xml:lang="de">NAME:de</name>
    <summary>SUMMARY:none</summary>
    <summary xml:lang="de">SUMMARY:de</summary>
    <description>
        <p>DESCRIPTION:none</p>
        <p xml:lang="de">DESCRIPTION:de</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")

        b.wait_text(".app-list .pf-v6-c-data-list__item-action[rowid='NAME:none'] button", "Install")
        b.click(".app-list .pf-v6-c-data-list__item-row button:contains('NAME:none')")
        b.wait_text(".app-description", "DESCRIPTION:none")

        def set_lang(lang: str) -> None:
            b.switch_to_top()
            b.open_session_menu()
            b.click("button.display-language-menu")
            b.wait_visible('#display-language-modal')
            b.click(f'#display-language-modal li[data-value={lang}] button')
            b.click("#display-language-modal footer button.pf-m-primary")
            b.wait_not_present('#display-language-modal')
            b.wait_language(lang)
            b.enter_page("/apps")

        set_lang("de-de")
        b.wait_text(".app-description", "DESCRIPTION:de")

        set_lang("ja-jp")
        b.wait_text(".app-description", "DESCRIPTION:none")

        # like in the general whitelist, but translated
        self.allow_journal_messages("xargs: basename: .*Signal 13.*")

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

        # Reset everything
        m.execute("for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info /usr/share/swcatalog; do mkdir -p $d; mount -t tmpfs tmpfs $d; done")
        self.addCleanup(m.execute, "for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info /usr/share/swcatalog; do umount $d; done")

        appstream_folder = "/usr/share/app-info/xmls"

        self.login_and_go("/apps")
        b.wait_visible(".pf-v6-c-empty-state")

        self.allow_journal_messages(".*/usr/share/app-info/xmls/test.xml.*",
                                    ".*xml.etree.ElementTree.ParseError.*")

        def reset():
            m.write(f"{appstream_folder}/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
            b.wait_not_present(".pf-v6-c-empty-state")
            b.wait_visible(".app-list .pf-v6-c-data-list__item-row:contains('Summary')")

        # First lay down some good XML so that we can later detect the reaction to broken XML.
        reset()

        # Unparsable
        m.write(f"{appstream_folder}/test.xml", """
This <is <not XML.
""")
        b.wait_visible(".pf-v6-c-empty-state")
        reset()

        # Not really AppStream
        m.write(f"{appstream_folder}/test.xml", """
<foo></foo>
""")
        b.wait_visible(".pf-v6-c-empty-state")
        reset()

        # No origin
        m.write(f"{appstream_folder}/test.xml", """
<components>
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
        b.wait_visible(".pf-v6-c-empty-state")
        reset()

        # No package
        m.write(f"{appstream_folder}/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
  </component>
</components>""")
        b.wait_visible(".pf-v6-c-empty-state")
        reset()

        # No id
        m.write(f"{appstream_folder}/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <name>Name</name>
    <summary>No description</summary>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
        b.wait_visible(".pf-v6-c-empty-state")
        reset()

        # Error (launchable without type) in earlier entry, shouldn't affect the later entry
        m.write(f"{appstream_folder}/test.xml", """
<components origin="test">
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test2</id>
    <name>Name</name>
    <summary>Summary</summary>
    <description>
        <p>Description 2</p>
    </description>
    <launchable>foo</launchable>
    <pkgname>foo</pkgname>
  </component>
  <component type="addon">
    <extends>org.cockpit_project.cockpit</extends>
    <id>org.cockpit-project.test</id>
    <name>Name</name>
    <summary>Summary 2</summary>
    <description>
        <p>Description</p>
    </description>
    <launchable type="cockpit-manifest">foo</launchable>
    <pkgname>foo</pkgname>
  </component>
</components>""")
        b.wait_not_present(".pf-v6-c-empty-state")
        b.wait_visible(".app-list .pf-v6-c-data-list__item-row:contains('Summary 2')")

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

        def disable_service(service_name: str) -> None:
            m.execute(f"systemctl stop {service_name}")
            system_service = m.execute(f"systemctl show -p FragmentPath {service_name}.service | cut -f2 -d=").strip()
            m.execute(f"mv {system_service} {system_service}.disabled")
            self.addCleanup(m.execute, f"mv {system_service}.disabled {system_service}")

        if m.image.startswith('fedora-'):
            disable_service("dnf5daemon-server")
            disable_service("packagekit")
        else:
            disable_service("packagekit")

        self.assertNotIn("\napps", m.execute("cockpit-bridge --packages"))
        self.login_and_go(None)
        b.wait_in_text("#host-apps", "Terminal")
        self.assertNotIn("Applications", b.text("#host-apps"))


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