from __future__ import annotations

import tempfile
import unittest
from pathlib import Path

from utils.cache.files import PROJECT_ROOT, read_text
from utils.cache.yaml import dump_yaml_str
from utils.roles.applications.services.registry import (
    GROUP_GATED_CONSTANTS,
    ServiceRegistryError,
    build_service_registry_from_applications,
    detect_service_bucket,
    detect_service_channel,
    is_explicit_truth,
    ordered_primary_service_entries,
    resolve_service_dependency_roles_from_config,
)


class TestServiceRegistryDiscovery(unittest.TestCase):
    """Per, the materialised application payload exposes services
    under the bare ``services`` key (no ``compose.services`` wrapper)."""

    def test_discovery_reads_provides_canonical_shared_and_enabled(self):
        applications = {
            "web-app-keycloak": {
                "services": {
                    "keycloak": {
                        "enabled": False,
                        "shared": True,
                        "provides": "oidc",
                    }
                }
            },
            "web-svc-cdn": {
                "services": {
                    "cdn": {"enabled": False, "shared": True},
                    "css": {"enabled": True, "shared": True, "canonical": "cdn"},
                    "javascript": {
                        "enabled": False,
                        "shared": True,
                        "canonical": "cdn",
                    },
                }
            },
        }

        registry = build_service_registry_from_applications(applications)

        self.assertEqual(registry["oidc"]["role"], "web-app-keycloak")
        self.assertEqual(registry["oidc"]["entity_name"], "keycloak")
        self.assertTrue(registry["oidc"]["shared"])
        self.assertFalse(registry["oidc"]["enabled"])

        self.assertEqual(registry["cdn"]["role"], "web-svc-cdn")
        self.assertNotIn("canonical", registry["cdn"])
        self.assertEqual(registry["css"]["canonical"], "cdn")
        self.assertTrue(registry["css"]["enabled"])
        self.assertTrue(registry["javascript"]["shared"])

    def test_entity_name_derivation_for_relevant_role_prefixes(self):
        applications = {
            "web-app-dashboard": {
                "services": {"dashboard": {"enabled": False, "shared": True}}
            },
            "web-svc-file": {"services": {"file": {"enabled": False, "shared": True}}},
            "svc-db-mariadb": {
                "services": {"mariadb": {"enabled": False, "shared": True}}
            },
            "svc-ai-ollama": {
                "services": {"ollama": {"enabled": False, "shared": True}}
            },
        }

        registry = build_service_registry_from_applications(applications)

        self.assertIn("dashboard", registry)
        self.assertIn("file", registry)
        self.assertIn("mariadb", registry)
        self.assertIn("ollama", registry)
        self.assertEqual(detect_service_channel("web-app-dashboard"), "frontend")
        self.assertEqual(detect_service_channel("svc-db-mariadb"), "backend")
        self.assertEqual(detect_service_bucket("web-app-dashboard"), "web-app")
        self.assertEqual(detect_service_bucket("web-svc-file"), "web-svc")
        self.assertEqual(detect_service_bucket("svc-db-mariadb"), "universal")

    def test_discovery_passes_overlay_block_through(self):
        applications = {
            "svc-db-mariadb": {
                "services": {"mariadb": {"enabled": True, "shared": True}},
                "networks": {
                    "overlay": {
                        "modes": ["compose", "swarm"],
                        "topology": "shared_net",
                        "aliases": ["mariadb"],
                        "consumer": {"kind": "database"},
                    }
                },
            },
            "svc-prx-openresty": {
                "services": {"openresty": {"enabled": True, "shared": True}},
                "networks": {
                    "overlay": {
                        "modes": ["swarm"],
                        "topology": "default_net",
                        "aliases": [],
                        "consumer": {
                            "kind": "services_flags",
                            "key": "sso",
                            "flags": ["enabled"],
                        },
                    }
                },
            },
            "web-app-keycloak": {
                "services": {
                    "keycloak": {
                        "enabled": True,
                        "shared": True,
                        "provides": "oidc",
                    }
                },
                "networks": {
                    "overlay": {
                        "modes": ["swarm"],
                        "proxy_resolvable": True,
                        "aliases": ["auth.example"],
                    }
                },
            },
            "web-app-plain": {"services": {"plain": {"enabled": True, "shared": True}}},
        }

        registry = build_service_registry_from_applications(applications)

        self.assertEqual(registry["mariadb"]["overlay"]["modes"], ["compose", "swarm"])
        self.assertEqual(registry["mariadb"]["overlay"]["topology"], "shared_net")
        self.assertEqual(registry["mariadb"]["overlay"]["consumer"]["kind"], "database")
        self.assertEqual(registry["openresty"]["overlay"]["modes"], ["swarm"])
        self.assertEqual(registry["openresty"]["overlay"]["aliases"], [])
        self.assertTrue(registry["oidc"]["overlay"]["proxy_resolvable"])
        self.assertNotIn("topology", registry["oidc"]["overlay"])
        self.assertEqual(registry["oidc"]["overlay"]["aliases"], ["auth.example"])
        self.assertNotIn("overlay", registry["plain"])

    def test_direct_service_resolution_uses_role_local_names(self):
        registry = {
            "ldap": {"role": "svc-db-openldap"},
            "mariadb": {"role": "svc-db-mariadb"},
        }
        config = {
            "services": {
                "ldap": {"enabled": True, "shared": True},
                "mariadb": {"enabled": True, "shared": True},
                "ignored": {"enabled": True, "shared": False},
            }
        }

        self.assertEqual(
            resolve_service_dependency_roles_from_config(config, registry),
            ["svc-db-openldap", "svc-db-mariadb"],
        )


class TestServiceRegistryOrdering(unittest.TestCase):
    """Per, ``run_after`` lives on the role's primary entity at
    ``meta/services.yml.<primary_entity>.run_after``."""

    def _mk_role(
        self,
        root: Path,
        role: str,
        primary_entity: str,
        services_payload: dict,
        *,
        run_after: list[str] | None = None,
    ) -> None:

        role_dir = root / role
        meta_dir = role_dir / "meta"
        meta_dir.mkdir(parents=True, exist_ok=True)

        services = dict(services_payload)
        if run_after is not None:
            primary = dict(services.get(primary_entity, {}) or {})
            if run_after:
                primary["run_after"] = list(run_after)
            services[primary_entity] = primary

        meta_dir.joinpath("services.yml").write_text(
            dump_yaml_str(services),
            encoding="utf-8",
        )
        meta_dir.joinpath("main.yml").write_text(
            "galaxy_info: {}\n",
            encoding="utf-8",
        )

    def test_ordered_entries_follow_bucket_order_and_run_after(self):
        registry = {
            "mariadb": {
                "role": "svc-db-mariadb",
                "bucket": "universal",
                "deploy_type": "universal",
            },
            "file": {
                "role": "web-svc-file",
                "bucket": "web-svc",
                "deploy_type": "server",
            },
            "asset": {
                "role": "web-svc-asset",
                "bucket": "web-svc",
                "deploy_type": "server",
            },
            "matomo": {
                "role": "web-app-matomo",
                "bucket": "web-app",
                "deploy_type": "server",
            },
            "dashboard": {
                "role": "web-app-dashboard",
                "bucket": "web-app",
                "deploy_type": "server",
            },
        }

        with tempfile.TemporaryDirectory() as td:
            roles_dir = Path(td)
            self._mk_role(
                roles_dir,
                "svc-db-mariadb",
                "mariadb",
                {"mariadb": {"enabled": False, "shared": True}},
            )
            self._mk_role(
                roles_dir,
                "web-svc-file",
                "file",
                {"file": {"enabled": False, "shared": True}},
            )
            self._mk_role(
                roles_dir,
                "web-svc-asset",
                "asset",
                {"asset": {"enabled": False, "shared": True}},
                run_after=["web-svc-file"],
            )
            self._mk_role(
                roles_dir,
                "web-app-matomo",
                "matomo",
                {"matomo": {"enabled": False, "shared": True}},
            )
            self._mk_role(
                roles_dir,
                "web-app-dashboard",
                "dashboard",
                {"dashboard": {"enabled": False, "shared": True}},
                run_after=["web-app-matomo"],
            )

            ordered = ordered_primary_service_entries(registry, roles_dir)

        self.assertEqual(
            [entry["role"] for entry in ordered],
            [
                "svc-db-mariadb",
                "web-svc-file",
                "web-svc-asset",
                "web-app-matomo",
                "web-app-dashboard",
            ],
        )

    def test_run_after_orders_across_buckets(self):
        registry = {
            "file": {
                "role": "web-svc-file",
                "bucket": "web-svc",
                "deploy_type": "server",
            },
            "mail": {
                "role": "sys-svc-mail",
                "bucket": "universal",
                "deploy_type": "universal",
            },
        }

        with tempfile.TemporaryDirectory() as td:
            roles_dir = Path(td)
            self._mk_role(
                roles_dir,
                "web-svc-file",
                "file",
                {"file": {"enabled": False, "shared": True}},
                run_after=["sys-svc-mail"],
            )
            self._mk_role(
                roles_dir,
                "sys-svc-mail",
                "mail",
                {"mail": {"enabled": False, "shared": True}},
            )

            ordered = ordered_primary_service_entries(registry, roles_dir)

        self.assertEqual(
            [entry["role"] for entry in ordered],
            ["sys-svc-mail", "web-svc-file"],
        )

    def test_circular_run_after_fails_hard(self):
        registry = {
            "file": {"role": "web-svc-file", "bucket": "web-svc"},
            "asset": {"role": "web-svc-asset", "bucket": "web-svc"},
        }

        with tempfile.TemporaryDirectory() as td:
            roles_dir = Path(td)
            self._mk_role(
                roles_dir,
                "web-svc-file",
                "file",
                {"file": {"enabled": False, "shared": True}},
                run_after=["web-svc-asset"],
            )
            self._mk_role(
                roles_dir,
                "web-svc-asset",
                "asset",
                {"asset": {"enabled": False, "shared": True}},
                run_after=["web-svc-file"],
            )

            with self.assertRaises(ServiceRegistryError):
                ordered_primary_service_entries(registry, roles_dir)


class TestExplicitTruth(unittest.TestCase):
    """A flag may resolve a dependency through a group-gated constant.

    The substring rule reads the role id out of the flag itself, so a SPOT
    constant hides it. Roles referencing one keep their provider only while
    the constant is registered.
    """

    def test_a_literal_true_resolves(self):
        self.assertTrue(is_explicit_truth(True))

    def test_the_in_group_names_form_resolves(self):
        self.assertTrue(is_explicit_truth("{{ 'web-app-keycloak' in group_names }}"))

    def test_every_registered_constant_resolves(self):
        for constant in GROUP_GATED_CONSTANTS:
            with self.subTest(constant=constant):
                self.assertTrue(is_explicit_truth("{{ " + constant + " }}"))

    def test_an_unregistered_jinja_expression_does_not_resolve(self):
        self.assertFalse(
            is_explicit_truth("{{ lookup('config', 'web-app-gitea', 'mcp.enabled') }}")
        )

    def test_a_literal_false_does_not_resolve(self):
        self.assertFalse(is_explicit_truth(False))

    def test_every_registered_constant_is_defined_in_group_vars(self):
        group_vars = PROJECT_ROOT / "group_vars" / "all"
        defined = "".join(read_text(str(path)) for path in group_vars.glob("*.yml"))
        for constant in GROUP_GATED_CONSTANTS:
            with self.subTest(constant=constant):
                self.assertIn(f"{constant}:", defined)


if __name__ == "__main__":
    unittest.main()
