"""Tests for the module dependency graph."""

from __future__ import annotations

import pytest

from ilum.core.modules import (
    MODULE_REGISTRY,
    ModuleCategory,
    ModuleResolver,
)
from ilum.errors import ModuleError


@pytest.fixture()
def resolver() -> ModuleResolver:
    return ModuleResolver()


class TestModuleRegistry:
    def test_required_modules(self) -> None:
        required = {m.name for m in MODULE_REGISTRY if m.required}
        assert required == {"core", "ui", "mongodb"}

    def test_all_modules_have_enable_flags(self) -> None:
        for mod in MODULE_REGISTRY:
            assert mod.enable_flags, f"{mod.name} has no enable_flags"

    def test_all_modules_have_disable_flags(self) -> None:
        for mod in MODULE_REGISTRY:
            assert mod.disable_flags, f"{mod.name} has no disable_flags"

    def test_all_modules_have_description(self) -> None:
        for mod in MODULE_REGISTRY:
            assert mod.description, f"{mod.name} has no description"

    def test_all_modules_have_chart_condition(self) -> None:
        for mod in MODULE_REGISTRY:
            assert mod.chart_condition, f"{mod.name} has no chart_condition"

    def test_all_modules_have_values_key(self) -> None:
        for mod in MODULE_REGISTRY:
            assert mod.values_key, f"{mod.name} has no values_key"

    def test_all_modules_have_pod_label(self) -> None:
        for mod in MODULE_REGISTRY:
            assert mod.pod_label, f"{mod.name} has no pod_label"

    def test_unique_names(self) -> None:
        names = [m.name for m in MODULE_REGISTRY]
        assert len(names) == len(set(names))

    def test_requires_reference_valid_modules(self) -> None:
        names = {m.name for m in MODULE_REGISTRY}
        for mod in MODULE_REGISTRY:
            for dep in mod.requires:
                assert dep in names, f"{mod.name} requires unknown module '{dep}'"

    def test_conflicts_reference_valid_modules(self) -> None:
        names = {m.name for m in MODULE_REGISTRY}
        for mod in MODULE_REGISTRY:
            for c in mod.conflicts_with:
                assert c in names, f"{mod.name} conflicts with unknown module '{c}'"

    def test_default_enabled_modules(self) -> None:
        defaults = {m.name for m in MODULE_REGISTRY if m.default_enabled}
        expected = {"core", "ui", "jupyter", "mongodb", "minio", "postgresql", "gitea"}
        assert defaults == expected

    def test_console_path_format(self) -> None:
        """Modules with a console_path must use the /external/<name>/ format."""
        for mod in MODULE_REGISTRY:
            if mod.console_path:
                assert mod.console_path.startswith("/external/"), (
                    f"{mod.name} console_path should start with /external/"
                )
                assert mod.console_path.endswith("/"), f"{mod.name} console_path should end with /"

    def test_web_ui_modules_have_console_path(self) -> None:
        """Modules known to have web UIs should have console_path set."""
        web_ui_modules = {
            "jupyter",
            "jupyterhub",
            "zeppelin",
            "airflow",
            "superset",
            "monitoring",
            "gitea",
            "mlflow",
            "n8n",
            "nifi",
            "kestra",
            "mageai",
            "streamlit",
            "langfuse",
            "trino",
            "minio",
        }
        registry_map = {m.name: m for m in MODULE_REGISTRY}
        for name in web_ui_modules:
            mod = registry_map[name]
            assert mod.console_path, f"{name} should have a console_path"


class TestModuleResolver:
    def test_get_known_module(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("sql")
        assert mod.name == "sql"

    def test_get_unknown_module_raises(self, resolver: ModuleResolver) -> None:
        with pytest.raises(ModuleError):
            resolver.get("nonexistent")

    def test_all_modules(self, resolver: ModuleResolver) -> None:
        assert len(resolver.all_modules()) == len(MODULE_REGISTRY)

    def test_by_category(self, resolver: ModuleResolver) -> None:
        notebooks = resolver.by_category(ModuleCategory.NOTEBOOK)
        names = {m.name for m in notebooks}
        assert "jupyter" in names
        assert "jupyterhub" in names
        assert "zeppelin" in names

    def test_resolve_enables_sql(self, resolver: ModuleResolver) -> None:
        flags = resolver.resolve_enables("sql")
        assert "ilum-sql.enabled=true" in flags
        assert "ilum-core.sql.enabled=true" in flags
        # SQL requires postgresql and core
        assert "postgresql.enabled=true" in flags
        assert "ilum-core.enabled=true" in flags

    def test_resolve_enables_langfuse_transitive(self, resolver: ModuleResolver) -> None:
        flags = resolver.resolve_enables("langfuse")
        assert "langfuse.enabled=true" in flags
        assert "postgresql.enabled=true" in flags
        assert "clickhouse.enabled=true" in flags

    def test_resolve_enables_trino_transitive(self, resolver: ModuleResolver) -> None:
        """Trino requires hive-metastore + sql, which require postgresql + core."""
        flags = resolver.resolve_enables("trino")
        assert "trino.enabled=true" in flags
        assert "ilum-sql.config.trino.enabled=true" in flags
        assert "ilum-hive-metastore.enabled=true" in flags
        assert "ilum-sql.enabled=true" in flags
        assert "postgresql.enabled=true" in flags
        assert "ilum-core.metastore.enabled=true" in flags

    def test_resolve_disables_simple(self, resolver: ModuleResolver) -> None:
        active = frozenset({"sql", "postgresql", "core"})
        flags = resolver.resolve_disables("sql", active)
        assert "ilum-sql.enabled=false" in flags
        assert "ilum-core.sql.enabled=false" in flags

    def test_resolve_disables_preserves_shared_deps(self, resolver: ModuleResolver) -> None:
        """Disabling SQL shouldn't disable postgresql if airflow still needs it."""
        active = frozenset({"sql", "airflow", "postgresql", "core"})
        flags = resolver.resolve_disables("sql", active)
        assert "ilum-sql.enabled=false" in flags
        # postgresql is still needed by airflow — should NOT be disabled
        assert "postgresql.enabled=false" not in flags

    def test_validate_hive_nessie_conflict(self, resolver: ModuleResolver) -> None:
        warnings = resolver.validate_combination(frozenset({"hive-metastore", "nessie"}))
        assert len(warnings) > 0
        assert any("conflict" in w.lower() for w in warnings)

    def test_validate_no_conflict(self, resolver: ModuleResolver) -> None:
        # Include all transitive deps so there are no missing-dep warnings
        mods = frozenset({"jupyter", "sql", "core", "postgresql"})
        warnings = resolver.validate_combination(mods)
        assert len(warnings) == 0

    def test_hive_metastore_enable_flags(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("hive-metastore")
        assert "ilum-hive-metastore.enabled=true" in mod.enable_flags
        assert "ilum-core.metastore.enabled=true" in mod.enable_flags
        assert "ilum-core.metastore.type=hive" in mod.enable_flags

    def test_nessie_enable_flags(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("nessie")
        assert "nessie.enabled=true" in mod.enable_flags
        assert "ilum-core.metastore.enabled=true" in mod.enable_flags
        assert "ilum-core.metastore.type=nessie" in mod.enable_flags

    def test_loki_enable_flags(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("loki")
        assert "global.logAggregation.enabled=true" in mod.enable_flags
        assert "global.logAggregation.loki.enabled=true" in mod.enable_flags
        assert "global.logAggregation.promtail.enabled=true" in mod.enable_flags

    def test_kafka_enable_flags(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("kafka")
        assert "kafka.enabled=true" in mod.enable_flags
        assert "ilum-core.communication.type=kafka" in mod.enable_flags


class TestResolveSelection:
    def test_returns_selected_as_is_when_complete(self, resolver: ModuleResolver) -> None:
        """All deps met, required included — no changes."""
        selected = ["core", "ui", "mongodb", "postgresql", "jupyter"]
        final, msgs = resolver.resolve_selection(selected)
        assert set(final) == set(selected)
        assert msgs == []

    def test_adds_required_modules(self, resolver: ModuleResolver) -> None:
        """If user somehow skips required, they're added back."""
        selected = ["postgresql", "jupyter"]
        final, msgs = resolver.resolve_selection(selected)
        assert "core" in final
        assert "ui" in final
        assert "mongodb" in final
        assert any("required" in m.lower() for m in msgs)

    def test_adds_transitive_deps(self, resolver: ModuleResolver) -> None:
        """Selecting langfuse auto-adds postgresql + clickhouse."""
        selected = ["core", "ui", "mongodb", "langfuse"]
        final, msgs = resolver.resolve_selection(selected)
        assert "postgresql" in final
        assert "clickhouse" in final
        assert any("clickhouse" in m for m in msgs)

    def test_trino_pulls_hive_sql_and_postgresql(self, resolver: ModuleResolver) -> None:
        """trino -> hive-metastore + sql -> postgresql + core (already present)."""
        selected = ["core", "ui", "mongodb", "trino"]
        final, msgs = resolver.resolve_selection(selected)
        assert "hive-metastore" in final
        assert "sql" in final
        assert "postgresql" in final

    def test_detects_conflicts(self, resolver: ModuleResolver) -> None:
        """hive-metastore + nessie conflict is reported."""
        selected = ["core", "ui", "mongodb", "postgresql", "hive-metastore", "nessie"]
        final, msgs = resolver.resolve_selection(selected)
        assert any("conflict" in m.lower() for m in msgs)

    def test_no_duplicates(self, resolver: ModuleResolver) -> None:
        """Multiple modules requiring postgresql don't duplicate it."""
        selected = ["core", "ui", "mongodb", "sql", "airflow"]
        final, msgs = resolver.resolve_selection(selected)
        assert final.count("postgresql") == 1


class TestDetectEnabledModules:
    def test_detect_enabled_basic(self, resolver: ModuleResolver) -> None:
        live_values = {"ilum-core": {"enabled": True}}
        enabled = resolver.detect_enabled_modules(live_values)
        assert "core" in enabled

    def test_detect_enabled_nested(self, resolver: ModuleResolver) -> None:
        live_values = {"global": {"lineage": {"enabled": True}}}
        enabled = resolver.detect_enabled_modules(live_values)
        assert "marquez" in enabled

    def test_detect_enabled_empty(self, resolver: ModuleResolver) -> None:
        enabled = resolver.detect_enabled_modules({})
        assert enabled == []

    def test_detect_enabled_false_value(self, resolver: ModuleResolver) -> None:
        live_values = {"ilum-jupyter": {"enabled": False}}
        enabled = resolver.detect_enabled_modules(live_values)
        assert "jupyter" not in enabled


class TestResolveDotted:
    def test_simple_path(self) -> None:
        data = {"a": {"b": {"c": 42}}}
        assert ModuleResolver._resolve_dotted(data, "a.b.c") == 42

    def test_missing_path(self) -> None:
        data = {"a": {"b": 1}}
        assert ModuleResolver._resolve_dotted(data, "a.x.y") is None

    def test_single_segment(self) -> None:
        data = {"enabled": True}
        assert ModuleResolver._resolve_dotted(data, "enabled") is True


class TestExtractModuleVersion:
    def test_string_image_with_tag(self) -> None:
        from ilum.core.modules import extract_module_version

        values = {"image": "ilum/core:6.7.0-RC2"}
        assert extract_module_version(values) == "6.7.0-RC2"

    def test_dict_image_with_tag(self) -> None:
        from ilum.core.modules import extract_module_version

        values = {"image": {"repository": "ilum/superset", "tag": "4.1.0.1"}}
        assert extract_module_version(values) == "4.1.0.1"

    def test_dict_image_with_registry(self) -> None:
        from ilum.core.modules import extract_module_version

        values = {"image": {"registry": "docker.io", "repository": "ilum/core", "tag": "6.0.5"}}
        assert extract_module_version(values) == "6.0.5"

    def test_no_image_key(self) -> None:
        from ilum.core.modules import extract_module_version

        assert extract_module_version({}) == ""

    def test_string_image_without_tag(self) -> None:
        from ilum.core.modules import extract_module_version

        values = {"image": "ilum/core"}
        assert extract_module_version(values) == ""

    def test_dict_image_without_tag(self) -> None:
        from ilum.core.modules import extract_module_version

        values = {"image": {"repository": "ilum/core"}}
        assert extract_module_version(values) == ""

    def test_numeric_tag_converted_to_string(self) -> None:
        from ilum.core.modules import extract_module_version

        values = {"image": {"repository": "bitnami/minio", "tag": 2024}}
        assert extract_module_version(values) == "2024"


class TestUnityCatalogFlags:
    """Unity Catalog must set metastore flags and declare conflicts."""

    def test_unity_catalog_enable_sets_metastore_type(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("unity-catalog")
        assert "ilum-unity-catalog.enabled=true" in mod.enable_flags
        assert "ilum-core.metastore.enabled=true" in mod.enable_flags
        assert "ilum-core.metastore.type=unity" in mod.enable_flags

    def test_unity_catalog_disable_flags(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("unity-catalog")
        assert "ilum-unity-catalog.enabled=false" in mod.disable_flags
        assert "ilum-core.metastore.enabled=false" in mod.disable_flags

    def test_unity_catalog_conflicts_with_hive_and_nessie(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("unity-catalog")
        assert "hive-metastore" in mod.conflicts_with
        assert "nessie" in mod.conflicts_with

    def test_hive_conflicts_with_unity_catalog(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("hive-metastore")
        assert "unity-catalog" in mod.conflicts_with

    def test_nessie_conflicts_with_unity_catalog(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("nessie")
        assert "unity-catalog" in mod.conflicts_with

    def test_validate_unity_hive_conflict(self, resolver: ModuleResolver) -> None:
        warnings = resolver.validate_combination(
            frozenset({"unity-catalog", "hive-metastore", "postgresql", "core"})
        )
        assert any("conflict" in w.lower() for w in warnings)

    def test_validate_unity_nessie_conflict(self, resolver: ModuleResolver) -> None:
        warnings = resolver.validate_combination(
            frozenset({"unity-catalog", "nessie", "postgresql"})
        )
        assert any("conflict" in w.lower() for w in warnings)


class TestTrinoFlags:
    """Trino must set SQL Viewer config flag and require sql module."""

    def test_trino_enable_sets_sql_trino_config(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("trino")
        assert "trino.enabled=true" in mod.enable_flags
        assert "ilum-sql.config.trino.enabled=true" in mod.enable_flags

    def test_trino_disable_clears_sql_trino_config(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("trino")
        assert "trino.enabled=false" in mod.disable_flags
        assert "ilum-sql.config.trino.enabled=false" in mod.disable_flags

    def test_trino_requires_sql_module(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("trino")
        assert "sql" in mod.requires
        assert "hive-metastore" in mod.requires


class TestGraphiteFlags:
    """Graphite must set core job graphite flag."""

    def test_graphite_enable_sets_core_job_flag(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("graphite")
        assert "graphite-exporter.graphite.enabled=true" in mod.enable_flags
        assert "ilum-core.job.graphite.enabled=true" in mod.enable_flags

    def test_graphite_disable_clears_core_job_flag(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("graphite")
        assert "graphite-exporter.graphite.enabled=false" in mod.disable_flags
        assert "ilum-core.job.graphite.enabled=false" in mod.disable_flags


class TestMissingDependencies:
    """Modules using PostgreSQL or Core must declare them as dependencies."""

    def test_mlflow_requires_postgresql(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("mlflow")
        assert "postgresql" in mod.requires

    def test_n8n_requires_postgresql(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("n8n")
        assert "postgresql" in mod.requires

    def test_marquez_requires_postgresql(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("marquez")
        assert "postgresql" in mod.requires

    def test_nifi_requires_core(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("nifi")
        assert "core" in mod.requires

    def test_streamlit_requires_core(self, resolver: ModuleResolver) -> None:
        mod = resolver.get("streamlit")
        assert "core" in mod.requires
