from __future__ import annotations

import unittest
import unittest.mock as mock
from unittest.mock import patch

from ansible.errors import AnsibleError

from plugins.lookup.service import LookupModule

_SERVICE_REGISTRY = {
    "matomo": {"role": "web-app-matomo"},
    "cdn": {"role": "web-svc-cdn"},
    "css": {"role": "web-svc-cdn", "canonical": "cdn"},
    "logout": {"role": "web-svc-logout"},
    "collab": {"role": "web-svc-collab"},
}


def _run(
    terms,
    applications,
    group_names,
    service_registry=None,
    static_registry=None,
    **kwargs,
):
    loader_patch = patch("plugins.lookup.service.lookup_loader")
    patches = [
        loader_patch,
        patch(
            "plugins.lookup.service.build_service_registry_from_roles_dir",
            return_value=static_registry or {},
        ),
    ]
    if service_registry is not None:
        patches.append(
            patch(
                "plugins.lookup.service.build_service_registry_from_applications",
                return_value=service_registry,
            )
        )
    started = [p.start() for p in patches]
    loader_mock = started[0]
    loader_mock.get.return_value = mock.MagicMock(run=lambda *_a, **_k: [applications])
    try:
        lm = LookupModule()
        lm._loader = mock.MagicMock()
        return lm.run(
            terms,
            variables={"group_names": group_names},
            **kwargs,
        )
    finally:
        for p in reversed(patches):
            p.stop()


class TestServiceDirect(unittest.TestCase):
    def setUp(self):
        self.applications = {
            "web-app-foo": {
                "services": {
                    "matomo": {"enabled": True, "shared": True},
                    "css": {"enabled": True, "shared": False},
                    "logout": {"enabled": False, "shared": True},
                }
            },
            "web-app-bar": {"services": {}},
        }

    def _get(self, term, group_names=None):
        return _run(
            [term],
            self.applications,
            group_names if group_names is not None else ["web-app-foo"],
            service_registry=_SERVICE_REGISTRY,
        )[0]

    def test_required_true_when_enabled_and_shared(self):
        self.assertTrue(self._get("matomo")["required"])

    def test_required_false_when_enabled_only(self):
        self.assertFalse(self._get("cdn")["required"])

    def test_required_false_when_shared_only(self):
        self.assertFalse(self._get("logout")["required"])

    def test_required_false_when_service_absent(self):
        self.assertFalse(self._get("collab")["required"])

    def test_required_false_when_group_names_do_not_match(self):
        self.assertFalse(self._get("matomo", ["web-app-unknown"])["required"])

    def test_required_true_when_any_app_qualifies(self):
        self.assertTrue(self._get("matomo", ["web-app-bar", "web-app-foo"])["required"])

    def test_enabled_shared_flags_are_aggregated_via_canonical_alias(self):
        result = self._get("cdn")
        self.assertTrue(result["enabled"])
        self.assertFalse(result["shared"])

    def test_lookup_by_role_name_uses_same_primary_service(self):
        by_key = self._get("cdn")
        by_role = self._get("web-svc-cdn")
        self.assertEqual(by_key, by_role)

    def test_multiple_terms(self):
        results = _run(
            ["matomo", "cdn", "logout"],
            self.applications,
            ["web-app-foo"],
            service_registry=_SERVICE_REGISTRY,
        )
        self.assertEqual(len(results), 3)
        self.assertTrue(results[0]["required"])
        self.assertFalse(results[1]["required"])
        self.assertFalse(results[2]["required"])

    def test_single_list_term_is_flattened(self):
        results = _run(
            [["matomo", "cdn", "logout"]],
            self.applications,
            ["web-app-foo"],
            service_registry=_SERVICE_REGISTRY,
        )
        self.assertEqual(len(results), 3)
        self.assertTrue(results[0]["required"])
        self.assertFalse(results[1]["required"])
        self.assertFalse(results[2]["required"])

    def test_local_true_when_enabled_and_not_shared(self):
        self.assertTrue(self._get("cdn")["local"])

    def test_local_false_when_enabled_and_shared(self):
        self.assertFalse(self._get("matomo")["local"])

    def test_local_false_when_shared_only(self):
        self.assertFalse(self._get("logout")["local"])

    def test_local_false_when_service_absent(self):
        self.assertFalse(self._get("collab")["local"])

    def test_local_false_when_group_names_do_not_match(self):
        self.assertFalse(self._get("cdn", ["web-app-unknown"])["local"])

    def test_local_and_required_are_mutually_exclusive(self):
        for term in ("matomo", "cdn", "logout", "collab"):
            result = self._get(term)
            if result["enabled"]:
                self.assertFalse(
                    result["local"] and result["required"],
                    msg=f"{term}: local and required must not both be True",
                )

    def test_empty_terms_returns_empty(self):
        self.assertEqual(
            _run(
                [],
                self.applications,
                ["web-app-foo"],
                service_registry=_SERVICE_REGISTRY,
            ),
            [],
        )


class TestServiceTransitive(unittest.TestCase):
    def setUp(self):
        self.applications = {
            "web-app-nextcloud": {"services": {"collab": {"enabled": True}}},
            "web-svc-collab": {
                "services": {
                    "matomo": {"enabled": True, "shared": True},
                }
            },
        }

    def test_transitive_required_resolved(self):
        result = _run(
            ["matomo"],
            self.applications,
            ["web-app-nextcloud"],
            service_registry=_SERVICE_REGISTRY,
        )[0]
        self.assertTrue(result["required"])

    def test_transitive_requires_shared_at_target(self):
        applications = {
            "web-app-nextcloud": {"services": {"collab": {"enabled": True}}},
            "web-svc-collab": {"services": {"matomo": {"enabled": True}}},
        }
        result = _run(
            ["matomo"],
            applications,
            ["web-app-nextcloud"],
            service_registry=_SERVICE_REGISTRY,
        )[0]
        self.assertFalse(result["required"])

    def test_direct_provider_is_not_required_without_shared_flag(self):
        result = _run(
            ["collab"],
            self.applications,
            ["web-app-nextcloud"],
            service_registry=_SERVICE_REGISTRY,
        )[0]
        self.assertFalse(result["required"])


class TestServiceDiscoveryWithoutExplicitRegistry(unittest.TestCase):
    def test_role_local_provider_metadata_is_discovered_from_applications(self):
        applications = {
            "web-app-dashboard": {
                "services": {
                    "dashboard": {"enabled": False, "shared": True},
                }
            },
            "web-app-foo": {
                "services": {
                    "dashboard": {"enabled": True, "shared": True},
                }
            },
        }

        result = _run(["dashboard"], applications, ["web-app-foo"])[0]
        self.assertEqual(result["role"], "web-app-dashboard")
        self.assertTrue(result["required"])

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

        oidc = _run(["oidc"], applications, ["web-app-foo"])[0]
        cdn = _run(["cdn"], applications, ["web-app-foo"])[0]

        self.assertEqual(oidc["role"], "web-app-keycloak")
        self.assertTrue(oidc["required"])
        self.assertEqual(cdn["role"], "web-svc-cdn")
        self.assertTrue(cdn["enabled"])
        self.assertFalse(cdn["required"])


class TestServiceCycleGuard(unittest.TestCase):
    def test_cycle_does_not_loop(self):
        applications = {
            "svc-a": {"services": {"svc-b": {"enabled": True}}},
            "svc-b": {"services": {"svc-a": {"enabled": True}}},
        }
        service_registry = {
            "svc-a": {"role": "svc-a"},
            "svc-b": {"role": "svc-b"},
            "logout": {"role": "web-svc-logout"},
        }
        result = _run(
            ["logout"],
            applications,
            ["svc-a"],
            service_registry=service_registry,
        )[0]
        self.assertFalse(result["required"])

    def test_cycle_found_if_service_present(self):
        applications = {
            "svc-a": {
                "services": {
                    "svc-b": {"enabled": True},
                    "logout": {"enabled": True, "shared": True},
                }
            },
            "svc-b": {"services": {"svc-a": {"enabled": True}}},
        }
        service_registry = {
            "svc-a": {"role": "svc-a"},
            "svc-b": {"role": "svc-b"},
            "logout": {"role": "web-svc-logout"},
        }
        result = _run(
            ["logout"],
            applications,
            ["svc-b"],
            service_registry=service_registry,
        )[0]
        self.assertTrue(result["required"])


class TestServiceErrors(unittest.TestCase):
    def test_raises_when_group_names_not_list(self):
        with (
            patch("plugins.lookup.service.lookup_loader") as loader_mock,
            self.assertRaises(AnsibleError),
        ):
            loader_mock.get.return_value = mock.MagicMock(run=lambda *_a, **_k: [{}])
            lm = LookupModule()
            lm._loader = mock.MagicMock()
            lm.run(
                ["matomo"],
                variables={"group_names": "not-a-list"},
            )

    def test_raises_when_term_empty(self):
        with self.assertRaises(AnsibleError):
            _run(["   "], {}, [], service_registry=_SERVICE_REGISTRY)

    def test_raises_when_term_unknown_in_both_registries(self):
        with self.assertRaises(AnsibleError):
            _run(["totally-unknown-key"], {}, [], service_registry=_SERVICE_REGISTRY)


class TestServiceStaticFallback(unittest.TestCase):
    def test_out_of_play_key_resolves_via_static_registry(self):
        result = _run(
            ["css"],
            {"web-app-foo": {"services": {}}},
            ["web-app-foo"],
            service_registry={"matomo": {"role": "web-app-matomo"}},
            static_registry=_SERVICE_REGISTRY,
        )[0]
        self.assertEqual(result["id"], "css")
        self.assertEqual(result["role"], "web-svc-cdn")
        self.assertFalse(result["enabled"])
        self.assertFalse(result["shared"])
        self.assertFalse(result["required"])
        self.assertFalse(result["local"])

    def test_out_of_play_alias_aggregates_canonical_consumer_flags(self):
        result = _run(
            ["css"],
            {"web-app-foo": {"services": {"cdn": {"enabled": True, "shared": True}}}},
            ["web-app-foo"],
            service_registry={"matomo": {"role": "web-app-matomo"}},
            static_registry=_SERVICE_REGISTRY,
        )[0]
        self.assertTrue(result["enabled"])
        self.assertTrue(result["required"])


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