#!/usr/bin/env python3
#
# Copyright (c) 2024 YunoHost Contributors
#
# This file is part of YunoHost (see https://yunohost.org)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import os
import tempfile
from subprocess import check_call

import pytest
from yunohost.app import app_setting
from yunohost.domain import _get_maindomain
from yunohost.firewall import firewall_list
from yunohost.permission import permission_delete, user_permission_list
from yunohost.utils.process import check_output
from yunohost.utils.resources import (
    AppResource,
    AppResourceClassesByType,
    AppResourceManager,
)

dummyfile = "/tmp/dummyappresource-testapp"


class DummyAppResource(AppResource):
    type = "dummy"

    default_properties = {
        "file": "/tmp/dummyappresource-__APP__",
        "content": "foo",
    }

    def provision_or_update(self, context):
        open(self.file, "w").write(self.content)

        if self.content == "forbiddenvalue":
            raise Exception("Emeged you used the forbidden value!1!£&")

    def deprovision(self, context):
        os.system(f"rm -f {self.file}")


AppResourceClassesByType["dummy"] = DummyAppResource


def setup_function(function):
    clean()

    os.system("mkdir /etc/yunohost/apps/testapp")
    os.system("echo 'id: testapp' > /etc/yunohost/apps/testapp/settings.yml")
    os.system("echo 'packaging_format = 2' > /etc/yunohost/apps/testapp/manifest.toml")
    os.system("echo 'id = \"testapp\"' >> /etc/yunohost/apps/testapp/manifest.toml")
    os.system("echo 'version = \"0.1\"' >> /etc/yunohost/apps/testapp/manifest.toml")
    os.system(
        "echo 'description.en = \"A dummy app to test app resources\"' >> /etc/yunohost/apps/testapp/manifest.toml"
    )


def teardown_function(function):
    clean()


def clean():
    os.system(f"rm -f {dummyfile}")
    os.system("rm -rf /etc/yunohost/apps/testapp")
    os.system("rm -rf /var/www/testapp")
    os.system("rm -rf /home/yunohost.app/testapp")
    os.system("apt remove lolcat sl nyancat influxdb2 >/dev/null 2>/dev/null")
    os.system("userdel testapp 2>/dev/null")

    for p in user_permission_list()["permissions"]:
        if p.startswith("testapp."):
            permission_delete(p, force=True, sync_perm=False)


def test_provision_dummy():
    current = {"resources": {}}
    wanted = {"resources": {"dummy": {}}}

    assert not os.path.exists(dummyfile)
    AppResourceManager("testapp", current=current, wanted=wanted).apply(
        rollback_and_raise_exception_if_failure=False
    )
    assert open(dummyfile).read().strip() == "foo"


def test_deprovision_dummy():
    current = {"resources": {"dummy": {}}}
    wanted = {"resources": {}}

    open(dummyfile, "w").write("foo")

    assert open(dummyfile).read().strip() == "foo"
    AppResourceManager("testapp", current=current, wanted=wanted).apply(
        rollback_and_raise_exception_if_failure=False
    )
    assert not os.path.exists(dummyfile)


def test_provision_dummy_nondefaultvalue():
    current = {"resources": {}}
    wanted = {"resources": {"dummy": {"content": "bar"}}}

    assert not os.path.exists(dummyfile)
    AppResourceManager("testapp", current=current, wanted=wanted).apply(
        rollback_and_raise_exception_if_failure=False
    )
    assert open(dummyfile).read().strip() == "bar"


def test_update_dummy():
    current = {"resources": {"dummy": {}}}
    wanted = {"resources": {"dummy": {"content": "bar"}}}

    open(dummyfile, "w").write("foo")

    assert open(dummyfile).read().strip() == "foo"
    AppResourceManager("testapp", current=current, wanted=wanted).apply(
        rollback_and_raise_exception_if_failure=False
    )
    assert open(dummyfile).read().strip() == "bar"


def test_update_dummy_failwithrollback():
    current = {"resources": {"dummy": {}}}
    wanted = {"resources": {"dummy": {"content": "forbiddenvalue"}}}

    open(dummyfile, "w").write("foo")

    assert open(dummyfile).read().strip() == "foo"
    with pytest.raises(Exception):
        AppResourceManager("testapp", current=current, wanted=wanted).apply(
            rollback_and_raise_exception_if_failure=True
        )
    assert open(dummyfile).read().strip() == "foo"


def test_resource_system_user():
    r = AppResourceClassesByType["system_user"]

    conf = {}

    assert os.system("getent passwd testapp 2>/dev/null") != 0

    r(conf, "testapp").provision_or_update()

    assert os.system("getent passwd testapp >/dev/null") == 0
    assert os.system("groups testapp | grep -q 'sftp.app'") != 0

    conf["allow_sftp"] = True
    r(conf, "testapp").provision_or_update()

    assert os.system("getent passwd testapp >/dev/null") == 0
    assert os.system("groups testapp | grep -q 'sftp.app'") == 0

    r(conf, "testapp").deprovision()

    assert os.system("getent passwd testapp 2>/dev/null") != 0


def test_resource_install_dir():
    r = AppResourceClassesByType["install_dir"]
    conf = {"owner": "nobody:rx", "group": "nogroup:rx"}

    # FIXME: should also check settings ?
    # FIXME: should also check automigrate from final_path
    # FIXME: should also test changing the install folder location ?

    assert not os.path.exists("/var/www/testapp")

    r(conf, "testapp").provision_or_update()

    assert os.path.exists("/var/www/testapp")
    unixperms = check_output("ls -ld /var/www/testapp").split()
    assert unixperms[0] == "dr-xr-x---"
    assert unixperms[2] == "nobody"
    assert unixperms[3] == "nogroup"

    conf["owner"] = "nobody:rwx"
    conf["group"] = "www-data:x"

    r(conf, "testapp").provision_or_update()

    assert os.path.exists("/var/www/testapp")
    unixperms = check_output("ls -ld /var/www/testapp").split()
    assert unixperms[0] == "drwx--x---"
    assert unixperms[2] == "nobody"
    assert unixperms[3] == "www-data"

    r(conf, "testapp").deprovision()

    assert not os.path.exists("/var/www/testapp")


def test_resource_data_dir():
    r = AppResourceClassesByType["data_dir"]
    conf = {"owner": "nobody:rx", "group": "nogroup:rx"}

    assert not os.path.exists("/home/yunohost.app/testapp")

    r(conf, "testapp").provision_or_update()

    assert os.path.exists("/home/yunohost.app/testapp")
    unixperms = check_output("ls -ld /home/yunohost.app/testapp").split()
    assert unixperms[0] == "dr-xr-x---"
    assert unixperms[2] == "nobody"
    assert unixperms[3] == "nogroup"

    conf["owner"] = "nobody:rwx"
    conf["group"] = "www-data:x"

    r(conf, "testapp").provision_or_update()

    assert os.path.exists("/home/yunohost.app/testapp")
    unixperms = check_output("ls -ld /home/yunohost.app/testapp").split()
    assert unixperms[0] == "drwx--x---"
    assert unixperms[2] == "nobody"
    assert unixperms[3] == "www-data"

    r(conf, "testapp").deprovision()

    # FIXME : implement and check purge option
    # assert not os.path.exists("/home/yunohost.app/testapp")


def test_resource_ports():
    r = AppResourceClassesByType["ports"]
    conf = {}

    assert not app_setting("testapp", "port")

    r(conf, "testapp").provision_or_update()

    assert app_setting("testapp", "port")

    r(conf, "testapp").deprovision()

    assert not app_setting("testapp", "port")


def test_resource_ports_several():
    r = AppResourceClassesByType["ports"]
    conf = {"main": {"default": 12345}, "foobar": {"default": 23456}}

    assert not app_setting("testapp", "port")
    assert not app_setting("testapp", "port_foobar")

    r(conf, "testapp").provision_or_update()

    assert app_setting("testapp", "port")
    assert app_setting("testapp", "port_foobar")

    r(conf, "testapp").deprovision()

    assert not app_setting("testapp", "port")
    assert not app_setting("testapp", "port_foobar")


def test_resource_ports_firewall():
    r = AppResourceClassesByType["ports"]
    conf = {"main": {"default": 12345}}

    r(conf, "testapp").provision_or_update()

    assert 12345 not in firewall_list(protocol="tcp")["tcp"]

    conf = {"main": {"default": 12345, "exposed": "TCP"}}

    r(conf, "testapp").provision_or_update()

    assert 12345 in firewall_list(protocol="tcp")["tcp"]

    r(conf, "testapp").deprovision()

    assert 12345 not in firewall_list(protocol="tcp")["tcp"]


def test_resource_database():
    r = AppResourceClassesByType["database"]
    conf = {"type": "mysql"}

    assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") != 0
    assert not app_setting("testapp", "db_name")
    assert not app_setting("testapp", "db_user")
    assert not app_setting("testapp", "db_pwd")

    r(conf, "testapp").provision_or_update()

    assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") == 0
    assert app_setting("testapp", "db_name")
    assert app_setting("testapp", "db_user")
    assert app_setting("testapp", "db_pwd")

    r(conf, "testapp").deprovision()

    assert os.system("mysqlshow 'testapp' >/dev/null 2>/dev/null") != 0
    assert not app_setting("testapp", "db_name")
    assert not app_setting("testapp", "db_user")
    assert not app_setting("testapp", "db_pwd")


def test_resource_apt():
    r = AppResourceClassesByType["apt"]
    conf = {
        "packages": "nyancat, sl",
        "extras": {
            "influxdb": {
                "repo": "deb https://repos.influxdata.com/debian stable main",
                "key": "https://repos.influxdata.com/influxdata-archive.key",
                "packages": "influxdb2",
            }
        },
    }

    assert os.system("dpkg --list | grep -q 'ii *nyancat '") != 0
    assert os.system("dpkg --list | grep -q 'ii *sl '") != 0
    assert os.system("dpkg --list | grep -q 'ii *influxdb2 '") != 0
    assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0
    assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0

    r(conf, "testapp").provision_or_update()

    assert os.system("dpkg --list | grep -q 'ii *nyancat '") == 0
    assert os.system("dpkg --list | grep -q 'ii *sl '") == 0
    assert os.system("dpkg --list | grep -q 'ii *influxdb2 '") == 0
    assert (
        os.system("dpkg --list | grep -q 'ii *lolcat '") != 0
    )  # Lolcat shouldnt be installed yet
    assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") == 0

    conf["packages"] += ", lolcat"
    r(conf, "testapp").provision_or_update()

    assert os.system("dpkg --list | grep -q 'ii *nyancat '") == 0
    assert os.system("dpkg --list | grep -q 'ii *sl '") == 0
    assert os.system("dpkg --list | grep -q 'ii *influxdb2 '") == 0
    assert os.system("dpkg --list | grep -q 'ii *lolcat '") == 0
    assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") == 0

    r(conf, "testapp").deprovision()

    assert os.system("dpkg --list | grep -q 'ii *nyancat '") != 0
    assert os.system("dpkg --list | grep -q 'ii *sl '") != 0
    assert os.system("dpkg --list | grep -q 'ii *influxdb2 '") != 0
    assert os.system("dpkg --list | grep -q 'ii *lolcat '") != 0
    assert os.system("dpkg --list | grep -q 'ii *testapp-ynh-deps '") != 0


def test_resource_permissions():
    maindomain = _get_maindomain()
    os.system(f"echo 'domain: {maindomain}' >> /etc/yunohost/apps/testapp/settings.yml")
    os.system("echo 'path: /testapp' >> /etc/yunohost/apps/testapp/settings.yml")

    # A manager object is required to set the label of the app...
    manager = AppResourceManager("testapp", current={}, wanted={"name": "Test App"})
    r = AppResourceClassesByType["permissions"]
    conf = {
        "main": {
            "url": "/",
            "allowed": "visitors",
        },
    }

    res = user_permission_list(full=True)["permissions"]
    # Nowadays there's always an implicit "main" perm but with default stuff such as empty url
    assert res["testapp.main"]["url"] is None
    assert res["testapp.main"]["allowed"] == []

    r(conf, "testapp", manager).provision_or_update()

    res = user_permission_list(full=True)["permissions"]
    assert "testapp.main" in res
    assert "visitors" in res["testapp.main"]["allowed"]
    assert res["testapp.main"]["url"] == "/"
    assert "testapp.admin" not in res

    conf["admin"] = {"url": "/admin", "allowed": ""}

    r(conf, "testapp", manager).provision_or_update()

    res = user_permission_list(full=True)["permissions"]

    assert "testapp.main" in list(res.keys())
    assert "visitors" in res["testapp.main"]["allowed"]
    assert res["testapp.main"]["url"] == "/"

    assert "testapp.admin" in res
    assert not res["testapp.admin"]["allowed"]
    assert res["testapp.admin"]["url"] == "/admin"

    conf["admin"]["url"] = "/adminpanel"

    r(conf, "testapp", manager).provision_or_update()

    res = user_permission_list(full=True)["permissions"]

    assert res["testapp.admin"]["url"] == "/adminpanel"

    r(conf, "testapp").deprovision()

    res = user_permission_list(full=True)["permissions"]

    assert "testapp.admin" not in res
    # The main permission is still forced to exist
    assert "testapp.main" in res


def test_resource_nodejs():
    manager = AppResourceManager(
        "testapp",
        current={},
        wanted={"name": "Test App", "integration": {"helpers_version": "2.1"}},
    )

    r = AppResourceClassesByType["nodejs"]
    assert not app_setting("testapp", "nodejs_version")
    conf = {
        "version": "20",
    }

    r(conf, "testapp", manager).provision_or_update()

    nodejs_version = app_setting("testapp", "nodejs_version")
    assert nodejs_version
    nodejs_dir = f"{r.N_INSTALL_DIR}/n/versions/node/{nodejs_version}/bin"
    assert os.path.exists(nodejs_dir)

    env = {
        "N_PREFIX": r.N_INSTALL_DIR,
        "PATH": f"{nodejs_dir}:{os.environ['PATH']}",
    }

    assert check_output("which node", env=env).startswith(nodejs_dir)
    installed_version = check_output("node --version", env=env)
    assert installed_version.startswith("v20.")
    with tempfile.TemporaryDirectory(prefix="ynh_") as d:
        # Install a random simple package to validate npm is in the path and working
        check_call(["npm", "install", "ansi-styles"], cwd=d, env=env)
        # FIXME: the resource should install stuff as non-root probably ?
        assert os.path.exists(f"{d}/node_modules/")

    r({}, "testapp", manager).deprovision()
    assert not app_setting("testapp", "nodejs_version")
    assert not os.path.exists(nodejs_dir)


def test_resource_ruby():
    os.system("echo '[integration]' >> /etc/yunohost/apps/testapp/manifest.toml")
    os.system(
        "echo 'helpers_version = \"2.1\"' >> /etc/yunohost/apps/testapp/manifest.toml"
    )

    r = AppResourceClassesByType["system_user"]
    r({}, "testapp").provision_or_update()

    r = AppResourceClassesByType["install_dir"]
    r({}, "testapp").provision_or_update()
    install_dir = app_setting("testapp", "install_dir")

    manager = AppResourceManager(
        "testapp",
        current={},
        wanted={"name": "Test App", "integration": {"helpers_version": "2.1"}},
    )

    r = AppResourceClassesByType["apt"]
    conf = {
        "packages": "make, gcc, libjemalloc-dev, libffi-dev, libyaml-dev, zlib1g-dev"
    }
    r(conf, "testapp", manager).provision_or_update()

    r = AppResourceClassesByType["ruby"]
    assert not app_setting("testapp", "ruby_version")
    conf = {
        "version": "3.3.5",
    }

    try:
        r(conf, "testapp", manager).provision_or_update()
    except Exception:
        os.system("tail -n 40 /tmp/ruby-build*.log")
        raise

    ruby_version = app_setting("testapp", "ruby_version")
    assert ruby_version
    ruby_dir = f"{r.RBENV_ROOT}/versions/testapp/bin"
    ruby_dir2 = f"{r.RBENV_ROOT}/versions/{ruby_version}/bin"
    assert os.path.exists(ruby_dir)
    assert os.path.exists(ruby_dir2)

    env = {
        "PATH": f"{ruby_dir}:{os.environ['PATH']}",
    }

    assert check_output("which ruby", env=env).startswith(ruby_dir)
    assert check_output("which gem", env=env).startswith(ruby_dir)
    assert "3.3.5" in check_output("ruby --version", env=env)
    with tempfile.TemporaryDirectory(prefix="ynh_") as d:
        # Install a random simple package to validate the path etc
        check_call(
            "gem install bundler passenger --no-document".split(), cwd=d, env=env
        )
        check_call(
            "bundle config set --local without 'development test'".split(),
            cwd=d,
            env=env,
        )
        # FIXME: the resource should install stuff as non-root probably ?

    r({}, "testapp", manager).deprovision()
    assert not app_setting("testapp", "ruby_version")
    assert not os.path.exists(ruby_dir)
    assert not os.path.exists(ruby_dir2)


def test_resource_go():
    os.system("echo '[integration]' >> /etc/yunohost/apps/testapp/manifest.toml")
    os.system(
        "echo 'helpers_version = \"2.1\"' >> /etc/yunohost/apps/testapp/manifest.toml"
    )

    r = AppResourceClassesByType["system_user"]
    r({}, "testapp").provision_or_update()

    r = AppResourceClassesByType["install_dir"]
    r({}, "testapp").provision_or_update()
    install_dir = app_setting("testapp", "install_dir")

    r = AppResourceClassesByType["go"]
    assert not app_setting("testapp", "go_version")
    conf = {
        "version": "1.22",
    }

    r(conf, "testapp").provision_or_update()

    go_version = app_setting("testapp", "go_version")
    assert go_version and go_version.startswith("1.22.")
    go_dir = f"{r.GOENV_ROOT}/versions/{go_version}/bin"
    assert os.path.exists(go_dir)

    env = {
        "PATH": f"{go_dir}:{os.environ['PATH']}",
    }

    assert check_output("go version", env=env).startswith(
        f"go version go{go_version} linux/"
    )

    with tempfile.TemporaryDirectory(prefix="ynh_") as d:
        with open(f"{d}/helloworld.go", "w") as f:
            f.write(
                """
                package main
                import "fmt"
                func main() { fmt.Println("hello world") }
            """
            )
        env["HOME"] = d
        check_call("go build helloworld.go".split(), cwd=d, env=env)
        assert os.path.exists(f"{d}/helloworld")
        assert "hello world" in check_output("./helloworld", cwd=d)

    r({}, "testapp").deprovision()
    assert not app_setting("testapp", "go_version")
    assert not os.path.exists(go_dir)


def test_resource_composer():
    os.system("echo '[integration]' >> /etc/yunohost/apps/testapp/manifest.toml")
    os.system(
        "echo 'helpers_version = \"2.1\"' >> /etc/yunohost/apps/testapp/manifest.toml"
    )

    r = AppResourceClassesByType["system_user"]
    r({}, "testapp").provision_or_update()

    r = AppResourceClassesByType["install_dir"]
    r({}, "testapp").provision_or_update()
    install_dir = app_setting("testapp", "install_dir")

    r = AppResourceClassesByType["apt"]
    manager = AppResourceManager(
        "testapp",
        current={},
        wanted={"name": "Test App", "integration": {"helpers_version": "2.1"}},
    )
    conf = {"packages": "php8.2-fpm"}
    r(conf, "testapp", manager).provision_or_update()

    r = AppResourceClassesByType["composer"]
    assert not app_setting("testapp", "composer_version")
    conf = {
        "version": "2.8.3",
    }

    r(conf, "testapp").provision_or_update()
    assert app_setting("testapp", "composer_version")
    assert os.path.exists(install_dir + "/composer.phar")

    r(conf, "testapp")._run_script(
        "test_composer_exec",
        f"cd {install_dir}; ynh_composer_exec require symfony/polyfill-mbstring 1.31.0",
    )

    assert os.path.exists(install_dir + "/.composer")
    assert os.path.exists(install_dir + "/vendor/symfony/polyfill-mbstring")

    r(conf, "testapp").deprovision()
    assert not app_setting("testapp", "composer_version")
    assert not os.path.exists(install_dir + "/composer.phar")
