from typing import (
    Optional,
)

from yaml import safe_load

from galaxy.exceptions import (
    RequestParameterInvalidException,
    RequestParameterMissingException,
)
from galaxy.managers.object_store_instances import (
    CreateInstancePayload,
    ModifyInstancePayload,
    ObjectStoreInstancesManager,
    TestUpdateInstancePayload,
    TestUpgradeInstancePayload,
    UpdateInstancePayload,
    UpdateInstanceSecretPayload,
    UpgradeInstancePayload,
    UserConcreteObjectStoreModel,
)
from galaxy.objectstore import UserObjectStoresAppConfig
from galaxy.objectstore.templates import ConfiguredObjectStoreTemplates
from galaxy.objectstore.templates.examples import get_example
from galaxy.objectstore.unittest_utils import app_config
from galaxy.util.config_templates import RawTemplateConfig
from .base import BaseTestCase

SIMPLE_FILE_SOURCE_NAME = "myobjectstore"
SIMPLE_FILE_SOURCE_DESCRIPTION = "a description of my object store"


class Config:
    object_store_templates: Optional[list[RawTemplateConfig]] = None
    object_store_templates_config_file: Optional[str] = None

    def __init__(self, templates: list[RawTemplateConfig]):
        self.object_store_templates = templates


def simple_vault_template(tmp_path):
    return {
        "id": "simple_vault",
        "name": "Simple Vault",
        "description": "This is a simple description",
        "configuration": {
            "type": "disk",
            "files_dir": str(tmp_path / "{{ user.username }}/{{ secrets.sec1 }}"),
        },
        "secrets": {
            "sec1": {
                "help": "This is some simple help.",
            },
        },
    }


def simple_vault_template_with_undefined_jinja_problem(tmp_path):
    return {
        "id": "simple_vault",
        "name": "Simple Vault",
        "description": "This is a simple description",
        "configuration": {
            "type": "disk",
            "files_dir": str(tmp_path / "{{ username }}/{{ secrets.sec1 }}"),  # should be user.username
        },
        "secrets": {
            "sec1": {
                "help": "This is some simple help.",
            },
        },
    }


SIMPLE_VAULT_CREATE_PAYLOAD = CreateInstancePayload(
    name=SIMPLE_FILE_SOURCE_NAME,
    description=SIMPLE_FILE_SOURCE_DESCRIPTION,
    template_id="simple_vault",
    template_version=0,
    variables={},
    secrets={"sec1": "foosec"},
)


def simple_variable_template(tmp_path):
    return {
        "id": "simple_variable",
        "name": "Simple Variable",
        "description": "This is a simple description",
        "configuration": {
            "type": "disk",
            "files_dir": str(tmp_path / "{{ user.username }}/{{ variables.var1 }}"),
        },
        "variables": {
            "var1": {
                "type": "path_component",
                "help": "This is some simple help.",
            },
        },
    }


UPGRADE_EXAMPLE = get_example("testing_multi_version_with_secrets.yml")
UPGRADE_INITIAL_PAYLOAD = CreateInstancePayload(
    name="My Upgradable Disk",
    template_id="secure_disk",
    template_version=0,
    secrets={
        "sec1": "moocow",
    },
    variables={},
)


class TestUserObjectStoreTestCase(BaseTestCase):
    manager: ObjectStoreInstancesManager

    def test_show(self, tmp_path):
        user_object_store = self._init_and_create_simple(tmp_path)
        user_object_store_showed = self.manager.show(self.trans, user_object_store.uuid)
        assert user_object_store_showed
        assert user_object_store_showed.uuid == user_object_store.uuid

    def test_simple_update(self, tmp_path):
        self._init_managers(tmp_path, config_dict=simple_variable_template(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_variable",
            template_version=0,
            variables={"var1": "originalvarval"},
            secrets={},
        )
        user_object_store = self._create_instance(create_payload)
        assert user_object_store.variables
        assert user_object_store.variables["var1"] == "originalvarval"

        update = UpdateInstancePayload(
            variables={
                "var1": "newval",
            }
        )

        # assert response includes updated variable as well as a fresh show()
        response = self._modify(user_object_store, update)
        assert response.variables
        assert response.variables["var1"] == "newval"

        user_object_store_showed = self.manager.show(self.trans, user_object_store.uuid)
        assert user_object_store_showed.variables
        assert user_object_store_showed.variables["var1"] == "newval"

    def test_update_errors_on_extra_variables(self, tmp_path):
        self._init_managers(tmp_path, config_dict=simple_variable_template(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_variable",
            template_version=0,
            variables={"var1": "originalvarval"},
            secrets={},
        )
        user_object_store = self._create_instance(create_payload)
        update = UpdateInstancePayload(
            variables={
                "var1": "newval",
                "extra_var": "ghostval",
            }
        )
        self._assert_modify_throws_exception(user_object_store, update, RequestParameterInvalidException)

    def test_update_errors_on_invalid_variable_type(self, tmp_path):
        self._init_managers(tmp_path, config_dict=simple_variable_template(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_variable",
            template_version=0,
            variables={"var1": "originalvarval"},
            secrets={},
        )
        user_object_store = self._create_instance(create_payload)
        update = UpdateInstancePayload(
            variables={
                "var1": 13,
            }
        )

        self._assert_modify_throws_exception(user_object_store, update, RequestParameterInvalidException)

    def test_hide(self, tmp_path):
        user_object_store = self._init_and_create_simple(tmp_path)

        user_object_store_showed = self.manager.show(self.trans, user_object_store.uuid)
        assert not user_object_store_showed.hidden

        hide = UpdateInstancePayload(hidden=True)
        self._modify(user_object_store, hide)

        user_object_store_showed = self.manager.show(self.trans, user_object_store.uuid)
        assert user_object_store_showed.hidden

    def test_deactivate(self, tmp_path):
        user_object_store = self._init_and_create_simple(tmp_path)

        user_object_store_showed = self.manager.show(self.trans, user_object_store.uuid)
        assert not user_object_store_showed.hidden
        assert user_object_store_showed.active
        assert not user_object_store_showed.purged

        deactivate = UpdateInstancePayload(active=False)
        self._modify(user_object_store, deactivate)

        user_object_store_showed = self.manager.show(self.trans, user_object_store.uuid)
        assert user_object_store_showed.hidden
        assert not user_object_store_showed.active
        assert not user_object_store_showed.purged

    def test_purge(self, tmp_path):
        self._init_managers(tmp_path, simple_vault_template(tmp_path))
        user_object_store = self._create_instance(SIMPLE_VAULT_CREATE_PAYLOAD)
        self._assert_secret_is(user_object_store, "sec1", "foosec")
        self.manager.purge_instance(self.trans, user_object_store.uuid)
        self._assert_secret_absent(user_object_store, "sec1")

    def test_update_secret(self, tmp_path):
        self._init_managers(tmp_path, simple_vault_template(tmp_path))
        user_object_store = self._create_instance(SIMPLE_VAULT_CREATE_PAYLOAD)
        self._assert_secret_is(user_object_store, "sec1", "foosec")
        update = UpdateInstanceSecretPayload(secret_name="sec1", secret_value="newvalue")
        self._modify(user_object_store, update)
        self._assert_secret_is(user_object_store, "sec1", "newvalue")

    def test_cannot_update_invalid_secret(self, tmp_path):
        self._init_managers(tmp_path, simple_vault_template(tmp_path))
        user_object_store = self._create_instance(SIMPLE_VAULT_CREATE_PAYLOAD)
        update = UpdateInstanceSecretPayload(secret_name="undefinedsec", secret_value="newvalue")
        self._assert_modify_throws_exception(user_object_store, update, RequestParameterInvalidException)

    def test_upgrade(self, tmp_path):
        user_object_store = self._init_upgrade_test_case(tmp_path)
        assert "sec1" in user_object_store.secrets
        assert "sec2" not in user_object_store.secrets
        self._assert_secret_is(user_object_store, "sec1", "moocow")
        self._assert_secret_absent(user_object_store, "foobarxyz")
        self._assert_secret_absent(user_object_store, "sec2")
        upgrade_to_1 = UpgradeInstancePayload(
            template_version=1,
            secrets={
                "sec1": "moocow",
                "sec2": "aftersec2",
            },
            variables={},
        )
        user_object_store = self._modify(user_object_store, upgrade_to_1)
        assert "sec1" in user_object_store.secrets
        assert "sec2" in user_object_store.secrets
        self._assert_secret_is(user_object_store, "sec1", "moocow")
        self._assert_secret_is(user_object_store, "sec2", "aftersec2")
        upgrade_to_2 = UpgradeInstancePayload(
            template_version=2,
            secrets={},
            variables={},
        )

        user_object_store = self._modify(user_object_store, upgrade_to_2)
        assert "sec1" not in user_object_store.secrets
        assert "sec2" in user_object_store.secrets
        self._assert_secret_absent(user_object_store, "sec1")
        self._assert_secret_is(user_object_store, "sec2", "aftersec2")

    def test_upgrade_does_not_allow_extra_variables(self, tmp_path):
        user_object_store = self._init_upgrade_test_case(tmp_path)
        upgrade_to_1 = UpgradeInstancePayload(
            template_version=1,
            variables={
                "extra_variable": "moocow",
            },
            secrets={
                "sec1": "moocow",
                "sec2": "aftersec2",
            },
        )

        self._assert_modify_throws_exception(user_object_store, upgrade_to_1, RequestParameterInvalidException)

    def test_upgrade_does_not_allow_extra_secrets(self, tmp_path):
        user_object_store = self._init_upgrade_test_case(tmp_path)
        upgrade_to_1 = UpgradeInstancePayload(
            template_version=1,
            variables={},
            secrets={
                "sec1": "moocow",
                "sec2": "aftersec2",
                "extrasec": "moo345",
            },
        )

        self._assert_modify_throws_exception(user_object_store, upgrade_to_1, RequestParameterInvalidException)

    def test_upgrade_fails_if_new_secrets_absent(self, tmp_path):
        user_object_store = self._init_upgrade_test_case(tmp_path)
        upgrade_to_1 = UpgradeInstancePayload(
            template_version=1,
            variables={},
            secrets={
                "sec1": "moocow",
            },
        )

        self._assert_modify_throws_exception(user_object_store, upgrade_to_1, RequestParameterMissingException)

    def test_status_valid(self, tmp_path):
        self.init_user_in_database()
        self._init_managers(tmp_path, simple_vault_template(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_vault",
            template_version=0,
            variables={},
            secrets={"sec1": "foosec"},
        )
        status = self.manager.plugin_status(self.trans, create_payload)
        assert status.connection
        assert not status.connection.is_not_ok
        assert not status.template_definition.is_not_ok
        assert status.template_settings
        assert not status.template_settings.is_not_ok

    def test_status_existing_valid(self, tmp_path):
        self.init_user_in_database()
        self._init_managers(tmp_path, simple_vault_template(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_vault",
            template_version=0,
            variables={},
            secrets={"sec1": "foosec"},
        )
        user_object_store = self._create_instance(create_payload)
        status = self.manager.plugin_status_for_instance(self.trans, user_object_store.uuid)
        assert status.connection
        assert not status.connection.is_not_ok
        assert not status.template_definition.is_not_ok
        assert status.template_settings
        assert not status.template_settings.is_not_ok

    def test_status_invalid_settings_undefined_variable(self, tmp_path):
        self.init_user_in_database()
        self._init_managers(tmp_path, config_dict=simple_vault_template_with_undefined_jinja_problem(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_vault",
            template_version=0,
            variables={},
            secrets={},
        )
        status = self.manager.plugin_status(self.trans, create_payload)
        assert not status.template_definition.is_not_ok
        assert status.template_settings
        assert status.template_settings.is_not_ok
        assert (
            "Problem with template definition causing invalid settings resolution" in status.template_settings.message
        )
        assert status.connection is None

    def test_status_update_valid(self, tmp_path):
        self._init_managers(tmp_path, config_dict=simple_variable_template(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_variable",
            template_version=0,
            variables={"var1": "originalvarval"},
            secrets={},
        )
        user_object_store = self._create_instance(create_payload)

        update = TestUpdateInstancePayload(
            variables={
                "var1": "newval",
            }
        )
        status = self.manager.test_modify_instance(self.trans, user_object_store.uuid, update)
        assert not status.template_definition.is_not_ok
        assert status.template_settings
        assert not status.template_settings.is_not_ok
        assert status.connection
        assert not status.connection.is_not_ok

    def test_status_upgrade_valid(self, tmp_path):
        user_object_store = self._init_upgrade_test_case(tmp_path)
        assert "sec1" in user_object_store.secrets
        assert "sec2" not in user_object_store.secrets
        self._assert_secret_is(user_object_store, "sec1", "moocow")
        self._assert_secret_absent(user_object_store, "foobarxyz")
        self._assert_secret_absent(user_object_store, "sec2")
        upgrade_to_1 = TestUpgradeInstancePayload(
            template_version=1,
            secrets={
                "sec1": "moocow",
                "sec2": "aftersec2",
            },
            variables={},
        )
        status = self.manager.test_modify_instance(self.trans, user_object_store.uuid, upgrade_to_1)
        assert not status.template_definition.is_not_ok
        assert status.template_settings
        assert not status.template_settings.is_not_ok
        assert status.connection
        assert not status.connection.is_not_ok

    def test_status_upgrade_invalid(self, tmp_path):
        user_object_store = self._init_invalid_upgrade_test_case(tmp_path)
        upgrade_to_1 = TestUpgradeInstancePayload(
            template_version=1,
            secrets={},
            variables={},
        )
        status = self.manager.test_modify_instance(self.trans, user_object_store.uuid, upgrade_to_1)
        assert not status.template_definition.is_not_ok
        assert status.template_settings
        assert status.template_settings.is_not_ok
        assert (
            "Problem with template definition causing invalid settings resolution" in status.template_settings.message
        )
        assert status.connection is None

    def _init_upgrade_test_case(self, tmp_path) -> UserConcreteObjectStoreModel:
        example_yaml_str = UPGRADE_EXAMPLE
        example_yaml_str.replace("/data", str(tmp_path))
        config = safe_load(example_yaml_str)
        self._init_managers(tmp_path, config)
        user_object_store = self._create_instance(UPGRADE_INITIAL_PAYLOAD)
        return user_object_store

    def _init_invalid_upgrade_test_case(self, tmp_path) -> UserConcreteObjectStoreModel:
        version_0 = simple_vault_template(tmp_path)
        version_0["version"] = 0
        version_1 = simple_vault_template_with_undefined_jinja_problem(tmp_path)
        version_1["version"] = 1
        version_1["id"] = version_0["id"]
        config_dict = [
            version_0,
            version_1,
        ]
        self._init_managers(tmp_path, config_dict=config_dict)
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_vault",
            template_version=0,
            variables={},
            secrets={"sec1": "foosec"},
        )
        user_object_store = self._create_instance(create_payload)
        return user_object_store

    def _read_secret(self, user_object_store: UserConcreteObjectStoreModel, secret_name: str) -> str:
        user_vault = self.trans.user_vault
        config_secret_key = f"object_store_config/{user_object_store.uuid}/{secret_name}"
        return user_vault.read_secret(config_secret_key)

    def _assert_secret_is(self, user_object_store: UserConcreteObjectStoreModel, secret_name: str, expected_value: str):
        assert self._read_secret(user_object_store, secret_name) == expected_value

    def _assert_secret_absent(self, user_object_store: UserConcreteObjectStoreModel, secret_name: str):
        sec_val = self._read_secret(user_object_store, secret_name)
        # deleting vs never inserted...
        assert sec_val in ["", None]

    def _init_and_create_simple(self, tmp_path) -> UserConcreteObjectStoreModel:
        self._init_managers(tmp_path, simple_vault_template(tmp_path))
        create_payload = CreateInstancePayload(
            name=SIMPLE_FILE_SOURCE_NAME,
            description=SIMPLE_FILE_SOURCE_DESCRIPTION,
            template_id="simple_vault",
            template_version=0,
            variables={},
            secrets={"sec1": "foosec"},
        )
        user_object_store = self._create_instance(create_payload)
        return user_object_store

    def _init_managers(self, tmp_path, config_dict=None):
        object_stores_config = app_config(tmp_path)
        self.app[UserObjectStoresAppConfig] = object_stores_config
        self.app.setup_test_vault()
        if isinstance(config_dict, dict):
            templates_config = Config([config_dict])
        else:
            templates_config = Config(config_dict)
        templates = ConfiguredObjectStoreTemplates.from_app_config(
            templates_config,
            vault_configured=True,
        )
        self.app[ConfiguredObjectStoreTemplates] = templates
        manager = self.app[ObjectStoreInstancesManager]
        self.manager = manager

    def _assert_modify_throws_exception(
        self,
        user_object_store: UserConcreteObjectStoreModel,
        modify: ModifyInstancePayload,
        exception_type: type[Exception],
    ):
        exception_thrown = False
        try:
            self._modify(user_object_store, modify)
        except exception_type:
            exception_thrown = True
        assert exception_thrown

    def _modify(
        self, user_object_store: UserConcreteObjectStoreModel, modify: ModifyInstancePayload
    ) -> UserConcreteObjectStoreModel:
        return self.manager.modify_instance(self.trans, user_object_store.uuid, modify)

    def _create_instance(self, create_payload: CreateInstancePayload) -> UserConcreteObjectStoreModel:
        user_object_store = self.manager.create_instance(self.trans, create_payload)
        return user_object_store
