"""
Tests for dynamic provider loading via entry points.

Covers: provider discovery, check discovery, check execution,
CLI argument registration, compliance frameworks, parser integration,
and all dispatch fallbacks for external providers.
"""

from argparse import Namespace
from unittest.mock import MagicMock, patch

import pytest

from prowler.providers.common.provider import Provider

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_entry_point(name, value, group):
    """Create a mock entry point."""
    ep = MagicMock()
    ep.name = name
    ep.value = value
    ep.group = group
    return ep


class FakeExternalProvider(Provider):
    """Minimal Provider subclass for testing the dynamic contract."""

    _type = "fakeexternal"
    _cli_help_text = "Fake External Provider"

    def __init__(self):
        Provider.set_global_provider(self)

    @property
    def type(self):
        return self._type

    @property
    def identity(self):
        return MagicMock(host_id="fake-host-1")

    @property
    def session(self):
        return MagicMock()

    @property
    def audit_config(self):
        return {}

    def setup_session(self):
        return MagicMock()

    def print_credentials(self):
        pass

    @classmethod
    def from_cli_args(cls, _arguments, _fixer_config):
        cls()

    def get_output_options(self, _arguments, _bulk_checks_metadata):
        return MagicMock(output_directory="/tmp", output_filename="fake")

    def get_stdout_detail(self, finding):
        return "fake-detail"

    def get_finding_sort_key(self):
        return "region"

    def get_summary_entity(self):
        return ("Fake Host", "fake-host-1")

    def get_finding_output_data(self, check_output):
        return {
            "auth_method": "fake",
            "account_uid": "fake-account",
            "account_name": "fake",
            "resource_name": "fake-resource",
            "resource_uid": "fake-uid",
            "region": "local",
        }

    def get_mutelist_finding_args(self):
        return {"host_id": self.identity.host_id}

    def display_compliance_table(
        self,
        findings,
        _bulk_checks_metadata,
        _compliance_framework,
        _output_filename,
        output_directory,  # referenced via name elsewhere in tests
        _compliance_overview,
    ):
        return True

    def get_html_assessment_summary(self):
        return "<div>Fake Assessment</div>"

    def generate_compliance_output(
        self,
        findings,
        _bulk_compliance_frameworks,
        _input_compliance_frameworks,
        output_options,
        generated_outputs,
    ):
        generated_outputs["compliance"].append("fake-compliance-output")

    @classmethod
    def init_parser(cls, parser_instance):
        pass


class FakeToolWrapperProvider(Provider):
    """External provider that declares itself a tool wrapper."""

    _type = "faketoolwrapper"
    is_external_tool_provider = True

    @property
    def type(self):
        return self._type

    @property
    def identity(self):
        return MagicMock()

    @property
    def session(self):
        return MagicMock()

    @property
    def audit_config(self):
        return {}

    def setup_session(self):
        return MagicMock()

    def print_credentials(self):
        pass


class FakePureContractProvider(Provider):
    """External provider that honors the from_cli_args type hint literally:
    returns an instance without calling Provider.set_global_provider() from
    __init__. Used to verify the call site wires the returned instance."""

    _type = "fakepure"

    @property
    def type(self):
        return self._type

    @property
    def identity(self):
        return MagicMock(host_id="fake-pure-1")

    @property
    def session(self):
        return MagicMock()

    @property
    def audit_config(self):
        return {}

    def setup_session(self):
        return MagicMock()

    def print_credentials(self):
        pass

    @classmethod
    def from_cli_args(cls, _arguments, _fixer_config):
        # Literal contract: return the instance, no side-effect in __init__.
        return cls()


class FakeProviderNoHelpText(Provider):
    """Provider without _cli_help_text."""

    _type = "nohelptext"

    @property
    def type(self):
        return self._type

    @property
    def identity(self):
        return MagicMock()

    @property
    def session(self):
        return MagicMock()

    @property
    def audit_config(self):
        return {}

    def setup_session(self):
        return MagicMock()

    def print_credentials(self):
        pass


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def _clear_ep_cache():
    """Clear the entry point provider cache before each test."""
    Provider._ep_providers = {}
    yield
    Provider._ep_providers = {}


@pytest.fixture
def fake_provider():
    """Create and register a FakeExternalProvider."""
    p = FakeExternalProvider()
    yield p
    Provider._global = None


# ===========================================================================
# 1. Provider Discovery & Loading
# ===========================================================================


class TestProviderDiscovery:
    """Tests 1-7: get_available_providers, _load_ep_provider, get_providers_help_text."""

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_get_available_providers_merges_builtin_and_entrypoint(self, mock_ep):
        """Test 1: get_available_providers returns both built-in and entry point providers."""
        mock_ep.return_value = [
            _make_entry_point("fakeexternal", "pkg.provider:Cls", "prowler.providers"),
        ]

        providers = Provider.get_available_providers()

        # Built-in providers from actual prowler package
        assert "aws" in providers
        # External provider from entry point
        assert "fakeexternal" in providers
        assert "common" not in providers

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_get_available_providers_deduplicates(self, mock_ep):
        """Test 2: Same provider name in built-in and entry point appears once."""
        # "aws" exists as built-in AND as entry point
        mock_ep.return_value = [
            _make_entry_point("aws", "pkg.provider:Cls", "prowler.providers"),
        ]

        providers = Provider.get_available_providers()

        assert providers.count("aws") == 1

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_load_ep_provider_loads_class(self, mock_ep):
        """Test 3: _load_ep_provider loads the class from entry point."""
        mock_ep.return_value = [
            _make_entry_point(
                "fakeexternal", "pkg:FakeExternalProvider", "prowler.providers"
            ),
        ]
        mock_ep.return_value[0].load.return_value = FakeExternalProvider

        cls = Provider._load_ep_provider("fakeexternal")

        assert cls is FakeExternalProvider

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_load_ep_provider_returns_none_for_unknown(self, mock_ep):
        """Test 4: _load_ep_provider returns None for unknown provider."""
        mock_ep.return_value = []

        cls = Provider._load_ep_provider("nonexistent")

        assert cls is None

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_load_ep_provider_caches_result(self, mock_ep):
        """Test 5: _load_ep_provider caches the loaded class."""
        mock_ep.return_value = [
            _make_entry_point("fakeexternal", "pkg:Cls", "prowler.providers"),
        ]
        mock_ep.return_value[0].load.return_value = FakeExternalProvider

        cls1 = Provider._load_ep_provider("fakeexternal")
        cls2 = Provider._load_ep_provider("fakeexternal")

        assert cls1 is cls2
        # load() should only be called once due to caching
        mock_ep.return_value[0].load.assert_called_once()

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_load_ep_provider_caches_misses(self, mock_ep):
        """A miss (unknown provider) must also be cached so repeated lookups
        do not re-iterate entry_points(). Aligns with tool_wrapper._ep_class_cache,
        which already caches None on miss."""
        mock_ep.return_value = []

        first = Provider._load_ep_provider("nonexistent")
        second = Provider._load_ep_provider("nonexistent")

        assert first is None
        assert second is None
        # entry_points() should only be called once across the two lookups
        mock_ep.assert_called_once()

    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_providers_help_text_reads_cli_help_text(
        self, mock_providers, mock_load
    ):
        """Test 6: get_providers_help_text reads _cli_help_text from entry point provider."""
        mock_providers.return_value = ["fakeexternal"]
        mock_load.return_value = FakeExternalProvider

        help_text = Provider.get_providers_help_text()

        assert help_text["fakeexternal"] == "Fake External Provider"

    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_providers_help_text_empty_without_cli_help_text(
        self, mock_providers, mock_load
    ):
        """Test 7: get_providers_help_text returns empty string without _cli_help_text."""
        mock_providers.return_value = ["nohelptext"]
        mock_load.return_value = FakeProviderNoHelpText

        help_text = Provider.get_providers_help_text()

        assert help_text["nohelptext"] == ""


class TestSdkOnly:
    """The ``sdk_only`` flag (default True) and Provider.get_app_providers."""

    def test_base_contract_defaults_to_sdk_only(self):
        # The default must be True so nothing leaks into the app implicitly.
        assert Provider.sdk_only is True

    def test_external_provider_without_flag_is_sdk_only(self):
        # FakeExternalProvider does not override the flag -> inherits True.
        assert FakeExternalProvider.sdk_only is True

    @patch("prowler.providers.common.provider.Provider.get_class")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_app_providers_filters_out_sdk_only(
        self, mock_available, mock_get_class
    ):
        app_cls = type("AppProvider", (Provider,), {"sdk_only": False})
        sdk_cls = type("SdkProvider", (Provider,), {"sdk_only": True})
        mock_available.return_value = ["appone", "sdkone", "apptwo"]
        mock_get_class.side_effect = lambda name: {
            "appone": app_cls,
            "sdkone": sdk_cls,
            "apptwo": app_cls,
        }[name]

        app_providers = Provider.get_app_providers()

        assert app_providers == ["appone", "apptwo"]

    @patch("prowler.providers.common.provider.Provider.get_class")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_app_providers_excludes_provider_that_fails_to_load(
        self, mock_available, mock_get_class
    ):
        # A provider whose class cannot be imported is treated as sdk_only
        # (excluded) so a broken plug-in never leaks into the app.
        app_cls = type("AppProvider", (Provider,), {"sdk_only": False})
        mock_available.return_value = ["appone", "broken"]

        def _get_class(name):
            if name == "broken":
                raise ImportError("missing transitive dep")
            return app_cls

        mock_get_class.side_effect = _get_class

        assert Provider.get_app_providers() == ["appone"]

    def test_app_exposed_builtins_declare_sdk_only_false(self):
        # The providers implemented end-to-end in the API/UI must opt in.
        app_providers = set(Provider.get_app_providers())
        for name in (
            "aws",
            "azure",
            "gcp",
            "kubernetes",
            "m365",
            "github",
            "mongodbatlas",
            "iac",
            "oraclecloud",
            "alibabacloud",
            "cloudflare",
            "openstack",
            "image",
            "googleworkspace",
            "vercel",
            "okta",
        ):
            assert name in app_providers, f"{name} should be exposed to the app"

    def test_sdk_only_builtins_are_hidden_from_app(self):
        # Built-ins not implemented in the API stay SDK-only via the default.
        app_providers = set(Provider.get_app_providers())
        for name in ("llm", "nhn", "scaleway", "stackit"):
            assert name not in app_providers, f"{name} must be hidden from the app"


class TestIsToolWrapperProvider:
    """Tests for Provider.is_tool_wrapper_provider — the helper that combines the
    built-in EXTERNAL_TOOL_PROVIDERS frozenset with the is_external_tool_provider
    class attribute of entry-point providers."""

    def test_returns_true_for_builtin_tool_wrappers(self):
        # iac/llm/image are in the EXTERNAL_TOOL_PROVIDERS frozenset (fast path)
        assert Provider.is_tool_wrapper_provider("iac") is True
        assert Provider.is_tool_wrapper_provider("llm") is True
        assert Provider.is_tool_wrapper_provider("image") is True

    def test_returns_false_for_regular_builtin_providers(self):
        # Regular built-ins must not be classified as tool wrappers
        assert Provider.is_tool_wrapper_provider("aws") is False
        assert Provider.is_tool_wrapper_provider("gcp") is False
        assert Provider.is_tool_wrapper_provider("github") is False

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_returns_true_for_external_provider_declaring_flag(self, mock_ep):
        # External plugin explicitly declares is_external_tool_provider = True
        mock_ep.return_value = [
            _make_entry_point("faketoolwrapper", "pkg:Cls", "prowler.providers"),
        ]
        mock_ep.return_value[0].load.return_value = FakeToolWrapperProvider

        assert Provider.is_tool_wrapper_provider("faketoolwrapper") is True

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_returns_false_for_external_provider_without_flag(self, mock_ep):
        # External plugin without the flag (default False) is treated as regular
        mock_ep.return_value = [
            _make_entry_point("fakeexternal", "pkg:Cls", "prowler.providers"),
        ]
        mock_ep.return_value[0].load.return_value = FakeExternalProvider

        assert Provider.is_tool_wrapper_provider("fakeexternal") is False

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_returns_false_for_unknown_provider(self, mock_ep):
        mock_ep.return_value = []

        assert Provider.is_tool_wrapper_provider("does-not-exist") is False

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_returns_false_for_none_provider(self, mock_ep):
        # Pydantic validators may pass None when values.get("Provider") is missing
        mock_ep.return_value = []

        assert Provider.is_tool_wrapper_provider(None) is False


class TestIsBuiltinProvider:
    """Tests for Provider.is_builtin — the helper that discriminates built-in
    providers from external ones before attempting the import, so transitive
    dependency failures in built-ins don't get silently re-routed to entry points."""

    def test_returns_true_for_builtin_provider(self):
        assert Provider.is_builtin("aws") is True
        assert Provider.is_builtin("github") is True

    def test_returns_false_for_unknown_provider(self):
        assert Provider.is_builtin("nonexistent_xyz") is False

    @patch("prowler.providers.common.provider.importlib.util.find_spec")
    def test_returns_false_when_find_spec_raises(self, mock_find_spec):
        # Certain namespace package edge cases raise ValueError/ImportError —
        # helper should swallow and return False rather than propagate.
        mock_find_spec.side_effect = ValueError("namespace package edge case")

        assert Provider.is_builtin("some_provider") is False


class TestInitProvidersParserBuiltinDependencyFailure:
    """Selective fail-loud: init captures failures silently, enforce emits
    warning for non-invoked and exits for the invoked broken provider."""

    @patch("sys.argv", ["prowler", "aws"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_builtin_with_missing_transitive_dep_fails_loudly(
        self, mock_import, mock_is_builtin
    ):
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        mock_import.side_effect = ImportError("No module named 'boto3'")

        parser = MagicMock()
        parser._providers = ["aws"]

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["aws"],
        ):
            init_providers_parser(parser)
            assert "aws" in parser._builtin_load_failures
            with pytest.raises(SystemExit):
                enforce_invoked_provider_loaded(parser)

    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.Provider._load_ep_provider")
    def test_external_provider_does_not_touch_builtin_path(
        self, mock_load_ep, mock_is_builtin
    ):
        from prowler.providers.common.arguments import init_providers_parser

        mock_is_builtin.return_value = False
        ext_cls = MagicMock()
        ext_cls.init_parser = MagicMock()
        mock_load_ep.return_value = ext_cls

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["fakeexternal"],
        ):
            init_providers_parser(parser)

        ext_cls.init_parser.assert_called_once_with(parser)

    @patch("sys.argv", ["prowler", "aws"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_unrelated_builtin_failure_does_not_abort_when_other_provider_invoked(
        self, mock_import, mock_is_builtin
    ):
        """Broken stackit + invoked aws → warning, no abort."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        aws_module = MagicMock()

        def import_side_effect(module_path):
            if "stackit" in module_path:
                raise ImportError("No module named 'stackit.objectstorage'")
            return aws_module

        mock_import.side_effect = import_side_effect

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["aws", "stackit"],
        ):
            init_providers_parser(parser)
            assert "stackit" in parser._builtin_load_failures
            enforce_invoked_provider_loaded(parser)

        aws_module.init_parser.assert_called_once_with(parser)

    @patch("sys.argv", ["prowler", "-h"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_no_provider_invoked_failure_does_not_abort(
        self, mock_import, mock_is_builtin
    ):
        """`prowler -h` + broken built-in → warning, help still renders."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        mock_import.side_effect = ImportError("No module named 'stackit.objectstorage'")

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["stackit"],
        ):
            init_providers_parser(parser)
            enforce_invoked_provider_loaded(parser)

    @patch("sys.argv", ["prowler", "microsoft365"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_invoked_microsoft365_alias_still_triggers_fail_loud(
        self, mock_import, mock_is_builtin
    ):
        """Alias `microsoft365 → m365` must be normalised before matching."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        mock_import.side_effect = ImportError("No module named 'msgraph'")

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["m365"],
        ):
            init_providers_parser(parser)
            with pytest.raises(SystemExit):
                enforce_invoked_provider_loaded(parser)

    @patch("sys.argv", ["prowler", "oci"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_invoked_oci_alias_still_triggers_fail_loud(
        self, mock_import, mock_is_builtin
    ):
        """Alias `oci → oraclecloud` must be normalised before matching."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        mock_import.side_effect = ImportError("No module named 'oci'")

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["oraclecloud"],
        ):
            init_providers_parser(parser)
            with pytest.raises(SystemExit):
                enforce_invoked_provider_loaded(parser)

    @patch("sys.argv", ["prowler", "--output-directory", "stackit"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_flag_value_matching_provider_name_not_treated_as_invoked(
        self, mock_import, mock_is_builtin
    ):
        """Flag-first invocation → invoked is 'aws' (default), not the flag's value."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        aws_module = MagicMock()

        def import_side_effect(module_path):
            if "stackit" in module_path:
                raise ImportError("No module named 'stackit.objectstorage'")
            return aws_module

        mock_import.side_effect = import_side_effect

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["aws", "stackit"],
        ):
            init_providers_parser(parser)
            enforce_invoked_provider_loaded(parser)

        aws_module.init_parser.assert_called_once_with(parser)

    @patch("sys.argv", ["prowler", "aws"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_invoked_builtin_non_import_error_fails_loudly(
        self, mock_import, mock_is_builtin
    ):
        """Non-ImportError in invoked provider → still fail-loud."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        mock_import.side_effect = RuntimeError("Unexpected error in aws init_parser")

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["aws"],
        ):
            init_providers_parser(parser)
            with pytest.raises(SystemExit):
                enforce_invoked_provider_loaded(parser)

    @patch("sys.argv", ["prowler", "aws"])
    @patch("prowler.providers.common.arguments.Provider.is_builtin")
    @patch("prowler.providers.common.arguments.import_module")
    def test_unrelated_builtin_non_import_error_does_not_abort(
        self, mock_import, mock_is_builtin
    ):
        """Non-ImportError in unrelated provider → warning, no abort."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        mock_is_builtin.return_value = True
        aws_module = MagicMock()

        def import_side_effect(module_path):
            if "stackit" in module_path:
                raise RuntimeError("Unexpected error in stackit init_parser")
            return aws_module

        mock_import.side_effect = import_side_effect

        parser = MagicMock()

        with patch(
            "prowler.providers.common.arguments.Provider.get_available_providers",
            return_value=["aws", "stackit"],
        ):
            init_providers_parser(parser)
            enforce_invoked_provider_loaded(parser)

        aws_module.init_parser.assert_called_once_with(parser)


class TestParseArgsOverrideAlignment:
    """Regression: `parse(args=...)` overrides sys.argv AFTER __init__ ran;
    the selective fail-loud must read argv at enforce time, not init time."""

    def test_enforce_reads_current_sys_argv_not_init_time_sys_argv(self):
        """Init with argv=['prowler','-h'] (no provider) captures stackit
        failure silently. Enforce with argv=['prowler','stackit'] must
        fail-loud — proving alignment under parse(args=...)."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        def import_side_effect(path):
            if "stackit" in path:
                raise ImportError("No module named 'stackit.objectstorage'")
            return MagicMock()

        parser = MagicMock()

        with (
            patch(
                "prowler.providers.common.arguments.Provider.is_builtin",
                return_value=True,
            ),
            patch(
                "prowler.providers.common.arguments.Provider.get_available_providers",
                return_value=["aws", "stackit"],
            ),
            patch(
                "prowler.providers.common.arguments.import_module",
                side_effect=import_side_effect,
            ),
        ):
            # Phase 1: __init__ with ambient argv = ['prowler', '-h']
            with patch("sys.argv", ["prowler", "-h"]):
                init_providers_parser(parser)
            # Failure captured silently — no SystemExit during init
            assert "stackit" in parser._builtin_load_failures

            # Phase 2: parse(args=...) overrode sys.argv → stackit invoked
            with patch("sys.argv", ["prowler", "stackit"]):
                with pytest.raises(SystemExit):
                    enforce_invoked_provider_loaded(parser)

    def test_enforce_reads_current_sys_argv_for_no_invocation(self):
        """Inverse: init's argv invokes stackit, but parse(args=['prowler',
        '-h']) overrides. Enforce must NOT fail-loud."""
        from prowler.providers.common.arguments import (
            enforce_invoked_provider_loaded,
            init_providers_parser,
        )

        def import_side_effect(path):
            if "stackit" in path:
                raise ImportError("No module named 'stackit.objectstorage'")
            return MagicMock()

        parser = MagicMock()

        with (
            patch(
                "prowler.providers.common.arguments.Provider.is_builtin",
                return_value=True,
            ),
            patch(
                "prowler.providers.common.arguments.Provider.get_available_providers",
                return_value=["aws", "stackit"],
            ),
            patch(
                "prowler.providers.common.arguments.import_module",
                side_effect=import_side_effect,
            ),
        ):
            # Phase 1: __init__ with ambient argv pretending stackit invoked
            with patch("sys.argv", ["prowler", "stackit"]):
                init_providers_parser(parser)
            assert "stackit" in parser._builtin_load_failures

            # Phase 2: parse(args=['prowler', '-h']) overrode sys.argv →
            # no provider invoked anymore → enforce must NOT exit
            with patch("sys.argv", ["prowler", "-h"]):
                enforce_invoked_provider_loaded(parser)


class TestInitGlobalProviderBuiltinDependencyFailure:
    """Same contract as TestInitProvidersParserBuiltinDependencyFailure but
    for the provider class import path in init_global_provider."""

    @patch("prowler.providers.common.provider.Provider.is_builtin")
    @patch("prowler.providers.common.provider.import_module")
    def test_builtin_with_missing_transitive_dep_fails_loudly(
        self, mock_import, mock_is_builtin
    ):
        mock_is_builtin.return_value = True
        mock_import.side_effect = ImportError("No module named 'boto3'")

        args = Namespace(
            provider="aws",
            fixer_config="config.yaml",
            config_file="config.yaml",
        )

        Provider._global = None
        with pytest.raises(SystemExit):
            Provider.init_global_provider(args)
        Provider._global = None

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    def test_load_ep_provider_handles_load_exception(self, mock_ep):
        """_load_ep_provider returns None when ep.load() raises."""
        ep = _make_entry_point("broken", "pkg:Cls", "prowler.providers")
        ep.load.side_effect = Exception("Import failed")
        mock_ep.return_value = [ep]

        cls = Provider._load_ep_provider("broken")

        assert cls is None

    @patch("prowler.providers.common.provider.import_module")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_providers_help_text_builtin_path(
        self, mock_providers, mock_is_builtin, mock_import
    ):
        """get_providers_help_text reads _cli_help_text from a built-in provider module."""
        import types

        mock_providers.return_value = ["fakebuiltin"]
        mock_is_builtin.return_value = True

        mock_cls = type(
            "FakebuiltinProvider", (Provider,), {"_cli_help_text": "Built-in Help"}
        )
        mock_module = types.ModuleType("fake_module")
        mock_module.FakebuiltinProvider = mock_cls
        mock_import.return_value = mock_module

        help_text = Provider.get_providers_help_text()

        assert help_text["fakebuiltin"] == "Built-in Help"

    @patch("prowler.providers.common.provider.import_module")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_providers_help_text_generic_exception(
        self, mock_providers, mock_import
    ):
        """get_providers_help_text handles generic exceptions with empty string."""
        mock_providers.return_value = ["broken"]
        mock_import.side_effect = RuntimeError("Unexpected error")

        help_text = Provider.get_providers_help_text()

        assert help_text["broken"] == ""


# ===========================================================================
# 2. Provider Initialization
# ===========================================================================


class TestProviderInitialization:
    """Tests 8-9: init_global_provider fallback to entry point."""

    @patch("prowler.providers.common.provider.load_and_validate_config_file")
    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.import_module")
    def test_init_global_provider_fallback_to_entry_point(
        self, mock_import, mock_load_ep, mock_config
    ):
        """Test 8: init_global_provider falls back to entry point when built-in fails."""
        mock_import.side_effect = ImportError("No built-in")
        mock_load_ep.return_value = FakeExternalProvider
        mock_config.return_value = {}

        args = Namespace(
            provider="fakeexternal",
            fixer_config="config.yaml",
            config_file="config.yaml",
        )

        Provider._global = None
        Provider.init_global_provider(args)

        assert isinstance(Provider._global, FakeExternalProvider)
        Provider._global = None

    @patch("prowler.providers.common.provider.load_and_validate_config_file")
    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.import_module")
    def test_init_global_provider_exits_for_unknown_provider(
        self, mock_import, mock_load_ep, mock_config
    ):
        """Test 9: init_global_provider exits when provider not found anywhere."""
        mock_import.side_effect = ImportError("No built-in")
        mock_load_ep.return_value = None
        mock_config.return_value = {}

        args = Namespace(
            provider="nonexistent",
            fixer_config="config.yaml",
            config_file="config.yaml",
        )

        with pytest.raises(SystemExit):
            Provider.init_global_provider(args)

    @patch("prowler.providers.common.provider.load_and_validate_config_file")
    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.import_module")
    def test_init_global_provider_wires_instance_returned_by_from_cli_args(
        self, mock_import, mock_load_ep, mock_config
    ):
        """A provider that implements from_cli_args as a pure function (returns
        the instance without calling set_global_provider from __init__) is
        correctly wired as the global provider by init_global_provider."""
        mock_import.side_effect = ImportError("No built-in")
        mock_load_ep.return_value = FakePureContractProvider
        mock_config.return_value = {}

        args = Namespace(
            provider="fakepure",
            fixer_config="config.yaml",
            config_file="config.yaml",
        )

        Provider._global = None
        Provider.init_global_provider(args)

        assert isinstance(Provider._global, FakePureContractProvider)
        Provider._global = None

    @pytest.mark.parametrize(
        "plugin_name",
        [
            "awsx",
            "aws_lite",
            "azure_gov",
            "gcp_org",
            "github_enterprise",
            "iac_v2",
        ],
    )
    @patch("prowler.providers.common.provider.load_and_validate_config_file")
    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.import_module")
    def test_init_global_provider_external_with_builtin_substring_uses_from_cli_args(
        self, mock_import, mock_load_ep, mock_config, plugin_name
    ):
        """Regression guard for the substring footgun in the dispatch chain.

        An external plug-in whose name contains a built-in substring
        (e.g. `awsx`, `aws_lite`, `azure_gov`, `gcp_org`, `github_enterprise`,
        `iac_v2`) MUST be routed to the dynamic else and instantiated via
        `from_cli_args` — not silently captured by the built-in branch whose
        name happens to be a substring of the plug-in name. See PR #10700
        review.
        """
        mock_import.side_effect = ImportError("No built-in")
        mock_load_ep.return_value = FakeExternalProvider
        mock_config.return_value = {}

        # Namespace deliberately omits the kwargs of any built-in branch
        # (no `aws_retries_max_attempts`, `az_cli_auth`, `personal_access_token`,
        # etc.). If equality dispatch is broken and the plug-in is misrouted to
        # a built-in branch, attribute access will raise and the global never
        # gets wired.
        args = Namespace(
            provider=plugin_name,
            fixer_config="config.yaml",
            config_file="config.yaml",
        )

        Provider._global = None
        Provider.init_global_provider(args)

        assert isinstance(Provider._global, FakeExternalProvider)
        Provider._global = None

    @patch("prowler.providers.common.provider.logger")
    @patch("prowler.providers.common.provider.load_and_validate_config_file")
    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    @patch("prowler.providers.common.provider.import_module")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    def test_init_global_provider_warns_when_plugin_shadowed_by_builtin(
        self, mock_is_builtin, mock_import, mock_entry_points, mock_config, mock_logger
    ):
        """Regression guard: when a plug-in registers a provider name that
        collides with a built-in, the BUILT-IN wins and a warning is emitted
        naming the shadowed plug-in. Shadow detection matches by entry-point
        name only — the plug-in is never `ep.load()`-ed just to warn, so its
        module code cannot run during a built-in run. See PR #10700 review
        (HugoPBrito, Alan-TheGentleman).
        """
        # Simulate a built-in `aws` that exists, AND a plug-in registered
        # under the same `aws` name via entry points.
        mock_is_builtin.return_value = True
        shadow_ep = MagicMock()
        shadow_ep.name = "aws"  # plug-in shadowing the built-in name
        mock_entry_points.return_value = [shadow_ep]
        mock_import.return_value = MagicMock(
            AwsProvider=MagicMock(side_effect=lambda **_kw: None)
        )
        mock_config.return_value = {}

        args = Namespace(
            provider="aws",
            fixer_config="config.yaml",
            config_file="config.yaml",
            aws_retries_max_attempts=3,
            role=None,
            session_duration=None,
            external_id=None,
            role_session_name=None,
            mfa=None,
            profile=None,
            region=None,
            excluded_region=None,
            organizations_role=None,
            scan_unused_services=False,
            resource_tag=None,
            resource_arn=None,
            mutelist_file=None,
        )

        Provider._global = None
        try:
            Provider.init_global_provider(args)
        except BaseException:
            # The AwsProvider mock is fake and the dispatch may sys.exit on
            # the simulated failure; we only care about the warning emitted
            # before the dispatch happens.
            pass
        finally:
            Provider._global = None

        # Warning was emitted naming the shadowed plug-in
        warning_msgs = [
            call.args[0]
            for call in mock_logger.warning.call_args_list
            if call.args and "Plug-in provider 'aws'" in call.args[0]
        ]
        assert warning_msgs, "expected a warning about the shadowed plug-in 'aws'"
        assert "IGNORED" in warning_msgs[0]
        # Shadow detected by name only — plug-in code never executed to warn
        shadow_ep.load.assert_not_called()


# ===========================================================================
# 3. Check Discovery
# ===========================================================================


class TestCheckDiscovery:
    """Tests 10-14: _recover_ep_checks, recover_checks_from_provider."""

    @patch("prowler.lib.check.utils.importlib.metadata.entry_points")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_ep_checks_discovers_checks(self, mock_spec, mock_ep):
        """Test 10: _recover_ep_checks discovers checks from entry points."""
        from prowler.lib.check.utils import _recover_ep_checks

        mock_ep.return_value = [
            _make_entry_point("my_check", "pkg.checks.my_check", "prowler.checks.fake"),
        ]
        mock_spec_obj = MagicMock()
        mock_spec_obj.origin = "/path/to/pkg/checks/my_check.py"
        mock_spec.return_value = mock_spec_obj

        checks = _recover_ep_checks("fake")

        assert len(checks) == 1
        assert checks[0][0] == "my_check"
        assert checks[0][1] == "/path/to/pkg/checks"

    @patch("prowler.lib.check.utils.importlib.metadata.entry_points")
    def test_recover_ep_checks_empty_without_entry_points(self, mock_ep):
        """Test 11: _recover_ep_checks returns empty list with no entry points."""
        from prowler.lib.check.utils import _recover_ep_checks

        mock_ep.return_value = []

        checks = _recover_ep_checks("fake")

        assert checks == []

    @patch("prowler.lib.check.utils.importlib.metadata.entry_points")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_ep_checks_handles_broken_entry_point(self, mock_spec, mock_ep):
        """Test 12: _recover_ep_checks handles failed entry points gracefully."""
        from prowler.lib.check.utils import _recover_ep_checks

        mock_ep.return_value = [
            _make_entry_point("broken_check", "pkg.broken", "prowler.checks.fake"),
        ]
        mock_spec.side_effect = Exception("Module not found")

        checks = _recover_ep_checks("fake")

        assert checks == []

    @patch("prowler.lib.check.utils._recover_ep_checks")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_checks_handles_external_provider_without_services(
        self, mock_find_spec, mock_ep_checks
    ):
        """Test 13: recover_checks_from_provider doesn't crash for external providers.

        With find_spec returning None (built-in package doesn't exist), discovery
        falls through to entry points cleanly — no ModuleNotFoundError catch
        needed.
        """
        from prowler.lib.check.utils import recover_checks_from_provider

        mock_find_spec.return_value = None  # not a built-in
        mock_ep_checks.return_value = [("ext_check", "/path/to/check")]

        checks = recover_checks_from_provider("fakeexternal")

        assert len(checks) == 1
        assert checks[0][0] == "ext_check"

    @patch("prowler.lib.check.utils._recover_ep_checks")
    @patch("prowler.lib.check.utils.list_modules")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_checks_combines_builtin_and_entry_points(
        self, mock_find_spec, mock_list_modules, mock_ep_checks
    ):
        """Test 14: recover_checks_from_provider combines built-in and entry point checks."""
        from prowler.lib.check.utils import recover_checks_from_provider

        mock_find_spec.return_value = MagicMock()  # built-in package exists

        # Simulate a built-in module
        builtin_module = MagicMock()
        builtin_module.name = "prowler.providers.aws.services.ec2.check_a.check_a"
        builtin_module.module_finder.path = "/builtin/path"
        mock_list_modules.return_value = [builtin_module]

        mock_ep_checks.return_value = [("check_b", "/external/path")]

        checks = recover_checks_from_provider("aws")

        check_names = [c[0] for c in checks]
        assert "check_a" in check_names
        assert "check_b" in check_names

    @patch("prowler.lib.check.utils.importlib.metadata.entry_points")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_ep_checks_filters_by_service(self, mock_spec, mock_ep):
        """Service filter keeps only entry points whose dotted path includes
        `.services.{service}.` — mirroring the built-in package filter."""
        from prowler.lib.check.utils import _recover_ep_checks

        mock_ep.return_value = [
            _make_entry_point(
                "container_has_no_root_user",
                "prowler_artifacts_dockerdesktop.services.container.container_has_no_root_user.container_has_no_root_user",
                "prowler.checks.dockerdesktop",
            ),
            _make_entry_point(
                "image_is_signed",
                "prowler_artifacts_dockerdesktop.services.image.image_is_signed.image_is_signed",
                "prowler.checks.dockerdesktop",
            ),
        ]
        mock_spec_obj = MagicMock()
        mock_spec_obj.origin = "/some/path/check.py"
        mock_spec.return_value = mock_spec_obj

        checks = _recover_ep_checks("dockerdesktop", service="container")

        assert len(checks) == 1
        assert checks[0][0] == "container_has_no_root_user"

    @patch("prowler.lib.check.utils.importlib.metadata.entry_points")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_ep_checks_without_service_returns_all(self, mock_spec, mock_ep):
        """Without a service filter, all entry points for the provider are returned."""
        from prowler.lib.check.utils import _recover_ep_checks

        mock_ep.return_value = [
            _make_entry_point(
                "container_has_no_root_user",
                "prowler_artifacts_dockerdesktop.services.container.container_has_no_root_user.container_has_no_root_user",
                "prowler.checks.dockerdesktop",
            ),
            _make_entry_point(
                "image_is_signed",
                "prowler_artifacts_dockerdesktop.services.image.image_is_signed.image_is_signed",
                "prowler.checks.dockerdesktop",
            ),
        ]
        mock_spec_obj = MagicMock()
        mock_spec_obj.origin = "/some/path/check.py"
        mock_spec.return_value = mock_spec_obj

        checks = _recover_ep_checks("dockerdesktop")

        assert len(checks) == 2

    @patch("prowler.lib.check.utils._recover_ep_checks")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_checks_external_provider_with_service(
        self, mock_find_spec, mock_ep_checks
    ):
        """External provider with --service: built-in package doesn't exist,
        but entry points are still consulted and return the requested service's
        checks. No premature sys.exit."""
        from prowler.lib.check.utils import recover_checks_from_provider

        mock_find_spec.return_value = None  # not a built-in
        mock_ep_checks.return_value = [("container_check", "/ext/path")]

        checks = recover_checks_from_provider("dockerdesktop", service="container")

        assert len(checks) == 1
        assert checks[0][0] == "container_check"
        mock_ep_checks.assert_called_once_with("dockerdesktop", "container")

    @patch("prowler.lib.check.utils._recover_ep_checks")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_checks_unknown_service_fails_cleanly(
        self, mock_find_spec, mock_ep_checks
    ):
        """A typo or unknown service (not in built-ins nor in entry points)
        fails with a clear error message, not a silent empty result."""
        from prowler.lib.check.utils import recover_checks_from_provider

        mock_find_spec.return_value = None
        mock_ep_checks.return_value = []

        with pytest.raises(SystemExit):
            recover_checks_from_provider("aws", service="typo_service")

    @patch("prowler.lib.check.utils._recover_ep_checks")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_checks_builtin_with_new_external_service(
        self, mock_find_spec, mock_ep_checks
    ):
        """Built-in provider with a new service added via entry points:
        the built-in package for that specific service doesn't exist (find_spec
        returns None), but entry points pick it up. The gate `if not service:`
        that previously skipped entry points when --service was passed is removed."""
        from prowler.lib.check.utils import recover_checks_from_provider

        mock_find_spec.return_value = None  # built-in for new_aws_service doesn't exist
        mock_ep_checks.return_value = [("new_check", "/ext/path")]

        checks = recover_checks_from_provider("aws", service="new_aws_service")

        assert len(checks) == 1
        assert checks[0][0] == "new_check"
        mock_ep_checks.assert_called_once_with("aws", "new_aws_service")

    @patch("prowler.lib.check.utils._recover_ep_checks")
    @patch("prowler.lib.check.utils.list_modules")
    @patch("prowler.lib.check.utils.importlib.util.find_spec")
    def test_recover_checks_surfaces_error_when_builtin_service_import_fails(
        self, mock_find_spec, mock_list_modules, mock_ep_checks
    ):
        """Regression guard: when a built-in service's package exists but one
        of its modules fails to import (e.g. a broken transitive dependency),
        the error must surface via the global exception handler — not be
        silently swallowed and replaced by an entry-point plug-in that happens
        to share a name. See PR #10700 review (HugoPBrito)."""
        from prowler.lib.check.utils import recover_checks_from_provider

        mock_find_spec.return_value = MagicMock()  # built-in service exists
        mock_list_modules.side_effect = ImportError("missing transitive dep: foo")

        # Even if a plug-in registers checks for the same service, it must NOT
        # silently take over — the import error wins.
        mock_ep_checks.return_value = [("evil_check", "/evil/path")]

        with pytest.raises(SystemExit):
            recover_checks_from_provider("aws", service="ec2")


# ===========================================================================
# 4. Check Execution
# ===========================================================================


class TestCheckExecution:
    """Tests 15-17: _resolve_check_module."""

    @patch("prowler.lib.check.check.importlib.util.find_spec")
    @patch("prowler.lib.check.check.import_check")
    def test_resolve_check_module_builtin_first(self, mock_import, mock_find_spec):
        """Test 15: _resolve_check_module resolves built-in checks first."""
        from prowler.lib.check.check import _resolve_check_module

        mock_module = MagicMock()
        mock_import.return_value = mock_module
        mock_find_spec.return_value = MagicMock()  # built-in package exists

        result = _resolve_check_module("aws", "ec2", "my_check")

        assert result is mock_module
        mock_import.assert_called_once_with(
            "prowler.providers.aws.services.ec2.my_check.my_check"
        )

    @patch("prowler.lib.check.check.importlib.util.find_spec")
    @patch("prowler.lib.check.check.import_check")
    def test_resolve_check_module_fallback_to_entry_point(
        self, mock_import_check, mock_find_spec
    ):
        """Test 16: _resolve_check_module falls back to entry point when built-in is absent."""
        from prowler.lib.check.check import _resolve_check_module

        mock_find_spec.return_value = None  # built-in does not exist

        mock_ext_module = MagicMock()
        ep = _make_entry_point(
            "my_check", "ext_pkg.checks.my_check", "prowler.checks.fake"
        )

        with (
            patch("importlib.metadata.entry_points", return_value=[ep]),
            patch("importlib.import_module", return_value=mock_ext_module) as mock_imp,
        ):
            result = _resolve_check_module("fake", "svc", "my_check")

        assert result is mock_ext_module
        mock_imp.assert_called_with("ext_pkg.checks.my_check")
        mock_import_check.assert_not_called()

    @patch("prowler.lib.check.check.importlib.util.find_spec")
    @patch("prowler.lib.check.check.import_check")
    def test_resolve_check_module_builtin_wins_over_entry_point(
        self, mock_import_check, mock_find_spec
    ):
        """Regression guard: when both a built-in and an entry-point check
        exist with the same CheckID, the BUILT-IN wins. Plug-ins extend
        Prowler with new checks but cannot silently override existing
        built-ins — a security tool prefers fail-loud predictability over
        permissive overrides. CheckMetadata.get_bulk applies the same
        precedence (first-write-wins) and emits a warning. See PR #10700
        review (HugoPBrito)."""
        from prowler.lib.check.check import _resolve_check_module

        mock_find_spec.return_value = MagicMock()  # built-in exists
        builtin_module = MagicMock()
        mock_import_check.return_value = builtin_module

        # Plug-in registers same CheckID — must NOT take precedence
        ep = _make_entry_point(
            "ec2_instance_public_ip",
            "plug_pkg.checks.ec2_instance_public_ip",
            "prowler.checks.aws",
        )

        with (
            patch("importlib.metadata.entry_points", return_value=[ep]),
            patch("importlib.import_module") as mock_imp,
        ):
            result = _resolve_check_module("aws", "ec2", "ec2_instance_public_ip")

        assert result is builtin_module
        mock_import_check.assert_called_once_with(
            "prowler.providers.aws.services.ec2.ec2_instance_public_ip.ec2_instance_public_ip"
        )
        # Plug-in must NOT be loaded when a built-in with the same CheckID exists
        mock_imp.assert_not_called()

    @patch("prowler.lib.check.check.importlib.metadata.entry_points")
    @patch("prowler.lib.check.check.importlib.util.find_spec")
    def test_resolve_check_module_raises_when_not_found(self, mock_find_spec, mock_ep):
        """Test 17: _resolve_check_module raises ModuleNotFoundError when both fail."""
        from prowler.lib.check.check import _resolve_check_module

        mock_find_spec.return_value = None
        mock_ep.return_value = []

        with pytest.raises(ModuleNotFoundError, match="not found"):
            _resolve_check_module("fake", "svc", "nonexistent_check")

    @patch("prowler.lib.check.check.importlib.util.find_spec")
    @patch("prowler.lib.check.check.import_check")
    def test_resolve_check_module_surfaces_error_when_builtin_import_fails(
        self, mock_import_check, mock_find_spec
    ):
        """Regression guard: when no plug-in entry-point overrides the
        check, a built-in whose module exists but fails to import (e.g.
        broken transitive dependency) MUST surface the real error instead
        of being silently treated as 'not found'. See PR #10700 review
        (HugoPBrito)."""
        from prowler.lib.check.check import _resolve_check_module

        mock_find_spec.return_value = MagicMock()  # built-in module exists
        mock_import_check.side_effect = ImportError("missing transitive dep: foo")

        # No plug-in override — the built-in's import failure must propagate
        with patch("importlib.metadata.entry_points", return_value=[]):
            with pytest.raises(ImportError, match="missing transitive dep"):
                _resolve_check_module("aws", "ec2", "ec2_instance_public_ip")


# ===========================================================================
# 5. CLI Arguments
# ===========================================================================


class TestCLIArguments:
    """Tests 18-19: init_providers_parser fallback."""

    @patch("prowler.providers.common.arguments.Provider._load_ep_provider")
    @patch("prowler.providers.common.arguments.Provider.get_available_providers")
    @patch("prowler.providers.common.arguments.import_module")
    def test_init_providers_parser_fallback_to_init_parser(
        self, mock_import, mock_providers, mock_load_ep
    ):
        """Test 18: init_providers_parser falls back to cls.init_parser for external providers."""
        from prowler.providers.common.arguments import init_providers_parser

        mock_providers.return_value = ["fakeexternal"]
        mock_import.side_effect = ImportError("No built-in arguments module")
        mock_load_ep.return_value = FakeExternalProvider

        parser_instance = MagicMock()

        # Should not raise
        init_providers_parser(parser_instance)

    @patch("prowler.providers.common.arguments.Provider._load_ep_provider")
    @patch("prowler.providers.common.arguments.Provider.get_available_providers")
    @patch("prowler.providers.common.arguments.import_module")
    def test_init_providers_parser_no_crash_without_init_parser(
        self, mock_import, mock_providers, mock_load_ep
    ):
        """Test 19: init_providers_parser doesn't crash if provider has no init_parser."""
        from prowler.providers.common.arguments import init_providers_parser

        mock_providers.return_value = ["nohelptext"]
        mock_import.side_effect = ImportError("No built-in")
        # FakeProviderNoHelpText has no init_parser
        mock_load_ep.return_value = FakeProviderNoHelpText

        parser_instance = MagicMock()

        # Should not raise
        init_providers_parser(parser_instance)

    @patch("prowler.providers.common.arguments.Provider._load_ep_provider")
    @patch("prowler.providers.common.arguments.Provider.get_available_providers")
    @patch("prowler.providers.common.arguments.import_module")
    def test_init_providers_parser_handles_init_parser_exception(
        self, mock_import, mock_providers, mock_load_ep
    ):
        """init_providers_parser handles exception when init_parser raises."""
        from prowler.providers.common.arguments import init_providers_parser

        mock_providers.return_value = ["fakeexternal"]
        mock_import.side_effect = ImportError("No built-in")

        broken_cls = MagicMock()
        broken_cls.init_parser.side_effect = RuntimeError("Parser init failed")
        mock_load_ep.return_value = broken_cls

        parser_instance = MagicMock()

        # Should not raise
        init_providers_parser(parser_instance)


# ===========================================================================
# 6. Compliance
# ===========================================================================


class TestCompliance:
    """Tests 20-23: compliance discovery and loading."""

    @patch("prowler.config.config.importlib.metadata.entry_points")
    def test_get_ep_compliance_dirs_discovers_dirs(self, mock_ep):
        """Test 20: _get_ep_compliance_dirs discovers compliance directories."""
        from prowler.config.config import _get_ep_compliance_dirs

        mock_module = MagicMock()
        mock_module.__path__ = ["/path/to/compliance"]
        ep = _make_entry_point("fakeexternal", "pkg.compliance", "prowler.compliance")
        ep.load.return_value = mock_module
        mock_ep.return_value = [ep]

        dirs = _get_ep_compliance_dirs()

        assert dirs["fakeexternal"] == ["/path/to/compliance"]

    @patch("prowler.config.config.importlib.metadata.entry_points")
    def test_get_ep_compliance_dirs_file_fallback(self, mock_ep):
        """_get_ep_compliance_dirs uses __file__ when module has no __path__."""
        from prowler.config.config import _get_ep_compliance_dirs

        mock_module = MagicMock(spec=[])
        mock_module.__file__ = "/path/to/compliance/__init__.py"
        del mock_module.__path__
        ep = _make_entry_point("ext", "pkg.compliance", "prowler.compliance")
        ep.load.return_value = mock_module
        mock_ep.return_value = [ep]

        dirs = _get_ep_compliance_dirs()

        assert dirs["ext"] == ["/path/to/compliance"]

    @patch("prowler.config.config.importlib.metadata.entry_points")
    def test_get_ep_compliance_dirs_handles_load_exception(self, mock_ep):
        """_get_ep_compliance_dirs handles ep.load() exception gracefully."""
        from prowler.config.config import _get_ep_compliance_dirs

        ep = _make_entry_point("broken", "pkg.compliance", "prowler.compliance")
        ep.load.side_effect = Exception("Load failed")
        mock_ep.return_value = [ep]

        dirs = _get_ep_compliance_dirs()

        assert dirs == {}

    @patch("prowler.config.config._get_ep_compliance_dirs")
    def test_get_available_compliance_includes_external(self, mock_dirs):
        """Test 21: get_available_compliance_frameworks includes external compliance."""
        import json
        import os
        import tempfile

        from prowler.config.config import get_available_compliance_frameworks

        # Create a temp dir with a compliance JSON
        with tempfile.TemporaryDirectory() as tmpdir:
            json_path = os.path.join(tmpdir, "custom_1.0_ext.json")
            with open(json_path, "w") as f:
                json.dump({"Framework": "Custom", "Provider": "ext"}, f)

            mock_dirs.return_value = {"ext": [tmpdir]}

            frameworks = get_available_compliance_frameworks("ext")

            assert "custom_1.0_ext" in frameworks

    @patch("prowler.config.config.importlib.metadata.entry_points")
    def test_get_available_compliance_includes_external_universal(self, mock_ep):
        """External universal frameworks under prowler.compliance.universal are
        listed, for a provider and for the provider=None case that feeds
        --compliance choices."""
        import json
        import os
        import tempfile

        from prowler.config.config import get_available_compliance_frameworks

        with tempfile.TemporaryDirectory() as tmpdir:
            framework = {
                "framework": "CustomUniversal",
                "name": "Custom Universal",
                "version": "1.0",
                "description": "Multi-provider",
                "requirements": [
                    {
                        "id": "1",
                        "name": "r",
                        "description": "d",
                        "checks": {"aws": ["c"]},
                    }
                ],
            }
            with open(os.path.join(tmpdir, "customuniversal_1.0.json"), "w") as f:
                json.dump(framework, f)

            module = MagicMock()
            module.__path__ = [tmpdir]
            ep = _make_entry_point(
                "anyname", "pkg.compliance", "prowler.compliance.universal"
            )
            ep.load.return_value = module
            mock_ep.side_effect = lambda group: (
                [ep] if group == "prowler.compliance.universal" else []
            )

            assert "customuniversal_1.0" in get_available_compliance_frameworks("aws")
            assert "customuniversal_1.0" in get_available_compliance_frameworks(None)

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_compliance_get_bulk_loads_external(self, mock_list_modules, mock_ep):
        """Test 22: Compliance.get_bulk loads external compliance JSON."""
        import json
        import os
        import tempfile

        from prowler.lib.check.compliance_models import Compliance

        mock_list_modules.return_value = []

        # Create a valid compliance JSON
        with tempfile.TemporaryDirectory() as tmpdir:
            json_data = {
                "Framework": "Custom",
                "Name": "Custom Framework",
                "Version": "1.0",
                "Provider": "fakeexternal",
                "Description": "Test framework",
                "Requirements": [],
            }
            json_path = os.path.join(tmpdir, "custom_1.0_fakeexternal.json")
            with open(json_path, "w") as f:
                json.dump(json_data, f)

            mock_module = MagicMock()
            mock_module.__path__ = [tmpdir]
            ep = _make_entry_point(
                "fakeexternal", "pkg.compliance", "prowler.compliance"
            )
            ep.load.return_value = mock_module
            mock_ep.return_value = [ep]

            bulk = Compliance.get_bulk("fakeexternal")

            assert "custom_1.0_fakeexternal" in bulk
            assert bulk["custom_1.0_fakeexternal"].Framework == "Custom"

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_compliance_get_bulk_skips_non_legacy_external_json(
        self, mock_list_modules, mock_ep
    ):
        """A universal-schema JSON registered under prowler.compliance is skipped,
        not aborting the run via sys.exit."""
        import json
        import os
        import tempfile

        from prowler.lib.check.compliance_models import Compliance

        mock_list_modules.return_value = []

        with tempfile.TemporaryDirectory() as tmpdir:
            json_data = {
                "framework": "Universal",
                "name": "Universal Framework",
                "version": "1.0",
                "description": "Multi-provider",
                "requirements": [
                    {
                        "id": "1",
                        "name": "r",
                        "description": "d",
                        "checks": {"aws": ["c"]},
                    }
                ],
            }
            with open(os.path.join(tmpdir, "universal_1.0.json"), "w") as f:
                json.dump(json_data, f)

            mock_module = MagicMock()
            mock_module.__path__ = [tmpdir]
            ep = _make_entry_point("aws", "pkg.compliance", "prowler.compliance")
            ep.load.return_value = mock_module
            mock_ep.return_value = [ep]

            bulk = Compliance.get_bulk("aws")

        assert "universal_1.0" not in bulk

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_compliance_get_bulk_file_fallback(self, mock_list_modules, mock_ep):
        """Compliance.get_bulk uses __file__ when external module has no __path__."""
        import json
        import os
        import tempfile

        from prowler.lib.check.compliance_models import Compliance

        mock_list_modules.return_value = []

        with tempfile.TemporaryDirectory() as tmpdir:
            json_data = {
                "Framework": "Custom",
                "Name": "Custom File Fallback",
                "Version": "1.0",
                "Provider": "fakeexternal",
                "Description": "Test",
                "Requirements": [],
            }
            json_path = os.path.join(tmpdir, "custom_file_fakeexternal.json")
            with open(json_path, "w") as f:
                json.dump(json_data, f)

            mock_module = MagicMock(spec=[])
            mock_module.__file__ = os.path.join(tmpdir, "__init__.py")
            del mock_module.__path__
            ep = _make_entry_point(
                "fakeexternal", "pkg.compliance", "prowler.compliance"
            )
            ep.load.return_value = mock_module
            mock_ep.return_value = [ep]

            bulk = Compliance.get_bulk("fakeexternal")

            assert "custom_file_fakeexternal" in bulk

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_compliance_get_bulk_handles_external_exception(
        self, mock_list_modules, mock_ep
    ):
        """Compliance.get_bulk handles exception when loading external compliance."""
        from prowler.lib.check.compliance_models import Compliance

        mock_list_modules.return_value = []

        ep = _make_entry_point("fakeexternal", "pkg.compliance", "prowler.compliance")
        ep.load.side_effect = Exception("Load failed")
        mock_ep.return_value = [ep]

        bulk = Compliance.get_bulk("fakeexternal")

        assert bulk == {}

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_compliance_get_bulk_builtin_wins_on_duplicate(
        self, mock_list_modules, mock_ep
    ):
        """Test 23: Compliance.get_bulk built-in wins on duplicate framework names."""
        import json
        import os
        import tempfile

        from prowler.lib.check.compliance_models import Compliance

        mock_list_modules.return_value = []
        mock_ep.return_value = []

        # If both exist with same key, built-in (loaded first) should win
        # Since we have no built-in modules mocked, just verify external loads
        # The actual dedup logic: `if name not in bulk_compliance_frameworks`
        with tempfile.TemporaryDirectory() as tmpdir:
            json_data = {
                "Framework": "CIS",
                "Name": "CIS Test",
                "Version": "1.0",
                "Provider": "fakeexternal",
                "Description": "Test",
                "Requirements": [],
            }
            with open(os.path.join(tmpdir, "dup_framework.json"), "w") as f:
                json.dump(json_data, f)

            mock_module = MagicMock()
            mock_module.__path__ = [tmpdir]
            ep = _make_entry_point(
                "fakeexternal", "pkg.compliance", "prowler.compliance"
            )
            ep.load.return_value = mock_module
            mock_ep.return_value = [ep]

            bulk = Compliance.get_bulk("fakeexternal")

            assert "dup_framework" in bulk

    @pytest.mark.parametrize(
        "provider, framework_segments",
        [
            # `cloud` is a substring of THREE built-in modules at once.
            ("cloud", ["alibabacloud", "cloudflare", "oraclecloud"]),
            ("git", ["github"]),
            ("work", ["googleworkspace"]),
            ("open", ["openstack"]),
        ],
    )
    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_compliance_get_bulk_matches_provider_segment_exactly(
        self, mock_list_modules, mock_ep, provider, framework_segments
    ):
        """Regression: a provider whose name is a substring of one or more
        framework modules must NOT load them. The old `provider in name`
        check captured overlapping built-ins (e.g. `cloud` matched
        alibabacloud, cloudflare and oraclecloud). See PR #10700 review
        (Alan-TheGentleman).
        """
        import json
        import os
        import tempfile

        from prowler.lib.check.compliance_models import Compliance

        mock_ep.return_value = []

        with tempfile.TemporaryDirectory() as tmpdir:
            # The substring path the old code would have read from.
            os.mkdir(os.path.join(tmpdir, provider))
            json_data = {
                "Framework": "Custom",
                "Name": f"Should not load for '{provider}'",
                "Version": "1.0",
                "Provider": provider,
                "Description": "Test",
                "Requirements": [],
            }
            with open(os.path.join(tmpdir, provider, "wrong.json"), "w") as f:
                json.dump(json_data, f)

            modules = []
            for segment in framework_segments:
                module = MagicMock()
                module.name = f"prowler.compliance.{segment}"
                module.module_finder.path = tmpdir
                modules.append(module)
            mock_list_modules.return_value = modules

            bulk = Compliance.get_bulk(provider)

            # Exact-segment match: the provider is not any of these modules.
            assert "wrong" not in bulk
            assert bulk == {}


# ===========================================================================
# 7. Parser
# ===========================================================================


class TestParser:
    """Tests 24-27: parser dynamic discovery."""

    @patch("prowler.lib.cli.parser.Provider.get_providers_help_text")
    @patch("prowler.lib.cli.parser.Provider.get_available_providers")
    def test_parser_discovers_new_providers(self, mock_providers, mock_help):
        """Test 24: Parser discovers providers not in known_providers."""
        from prowler.lib.cli.parser import ProwlerArgumentParser

        mock_providers.return_value = [
            "aws",
            "azure",
            "gcp",
            "kubernetes",
            "m365",
            "github",
            "googleworkspace",
            "cloudflare",
            "oraclecloud",
            "openstack",
            "alibabacloud",
            "iac",
            "llm",
            "image",
            "nhn",
            "mongodbatlas",
            "fakeexternal",
        ]
        mock_help.return_value = {"fakeexternal": "Fake External Provider"}

        parser = ProwlerArgumentParser()

        assert "fakeexternal" in parser.parser.format_usage()

    @patch("prowler.lib.cli.parser.Provider.get_providers_help_text")
    @patch("prowler.lib.cli.parser.Provider.get_available_providers")
    def test_parser_appends_to_epilog_with_help_text(self, mock_providers, mock_help):
        """Test 25: Parser appends new providers to epilog with _cli_help_text."""
        from prowler.lib.cli.parser import ProwlerArgumentParser

        mock_providers.return_value = [
            "aws",
            "azure",
            "gcp",
            "kubernetes",
            "m365",
            "github",
            "googleworkspace",
            "cloudflare",
            "oraclecloud",
            "openstack",
            "alibabacloud",
            "iac",
            "llm",
            "image",
            "nhn",
            "mongodbatlas",
            "fakeexternal",
        ]
        mock_help.return_value = {"fakeexternal": "Fake External Provider"}

        parser = ProwlerArgumentParser()
        epilog = parser.parser.epilog

        assert "fakeexternal" in epilog
        assert "Fake External Provider" in epilog

    @patch("prowler.lib.cli.parser.Provider.get_providers_help_text")
    @patch("prowler.lib.cli.parser.Provider.get_available_providers")
    def test_parser_skips_epilog_entry_without_help_text(
        self, mock_providers, mock_help
    ):
        """Test 26: Parser doesn't add epilog entry if _cli_help_text is empty."""
        from prowler.lib.cli.parser import ProwlerArgumentParser

        mock_providers.return_value = [
            "aws",
            "azure",
            "gcp",
            "kubernetes",
            "m365",
            "github",
            "googleworkspace",
            "cloudflare",
            "oraclecloud",
            "openstack",
            "alibabacloud",
            "iac",
            "llm",
            "image",
            "nhn",
            "mongodbatlas",
            "nohelptext",
        ]
        mock_help.return_value = {"nohelptext": ""}

        parser = ProwlerArgumentParser()
        epilog = parser.parser.epilog

        # Should appear in usage/csv but NOT in the descriptive epilog listing
        assert "nohelptext" in parser.parser.format_usage()
        # No line with "nohelptext    Something" in epilog
        epilog_lines = [
            line.strip() for line in epilog.splitlines() if "nohelptext" in line
        ]
        assert len(epilog_lines) == 0 or all(
            "nohelptext" in line and line.strip() == "nohelptext" or "{" in line
            for line in epilog_lines
        )

    @patch("prowler.lib.cli.parser.Provider.get_providers_help_text")
    @patch("prowler.lib.cli.parser.Provider.get_available_providers")
    def test_parser_does_not_duplicate_known_providers(self, mock_providers, mock_help):
        """Test 27: Parser doesn't duplicate providers already in the known list."""
        from prowler.lib.cli.parser import ProwlerArgumentParser

        # No new providers
        mock_providers.return_value = [
            "aws",
            "azure",
            "gcp",
            "kubernetes",
            "m365",
            "github",
            "googleworkspace",
            "cloudflare",
            "oraclecloud",
            "openstack",
            "alibabacloud",
            "iac",
            "llm",
            "image",
            "nhn",
            "mongodbatlas",
        ]
        mock_help.return_value = {}

        parser = ProwlerArgumentParser()
        usage = parser.parser.format_usage()

        # aws should appear exactly once in usage
        assert usage.count("aws") == 1


# ===========================================================================
# 8. Dispatch Fallbacks
# ===========================================================================


class TestDispatchFallbacks:
    """Tests 28-34: all else clause fallbacks for external providers."""

    def test_stdout_report_calls_get_stdout_detail(self, fake_provider):
        """Test 28: stdout_report else clause calls provider.get_stdout_detail."""
        from prowler.lib.outputs.outputs import stdout_report

        finding = MagicMock()
        finding.check_metadata.Provider = "fakeexternal"
        finding.status = "FAIL"
        finding.muted = False
        finding.status_extended = "test"

        with patch("builtins.print") as mock_print:
            stdout_report(
                finding, "\033[31m", True, ["FAIL"], False, provider=fake_provider
            )

        mock_print.assert_called_once()
        printed = mock_print.call_args[0][0]
        assert "fake-detail" in printed

    def test_stdout_report_resolves_provider_when_none(self, fake_provider):
        """stdout_report resolves provider via get_global_provider when not passed."""
        from prowler.lib.outputs.outputs import stdout_report

        finding = MagicMock()
        finding.check_metadata.Provider = "fakeexternal"
        finding.status = "FAIL"
        finding.muted = False
        finding.status_extended = "test"

        with patch("builtins.print") as mock_print:
            stdout_report(finding, "\033[31m", True, ["FAIL"], False)

        mock_print.assert_called_once()
        printed = mock_print.call_args[0][0]
        assert "fake-detail" in printed

    def test_report_propagates_provider_to_stdout_report(self):
        """Regression guard: report() must pass its local `provider` through to
        stdout_report so the dynamic else does not fall back to the global
        singleton. With Provider._global cleared, the call chain still has to
        work for an external provider — proving the argument is being used
        instead of `Provider.get_global_provider()`. See PR #10700 review
        (HugoPBrito)."""
        from prowler.lib.outputs.outputs import report
        from prowler.providers.common.provider import Provider

        local_provider = FakeExternalProvider.__new__(FakeExternalProvider)

        finding = MagicMock()
        finding.status = "PASS"
        finding.muted = False
        finding.region = "x"
        finding.check_metadata.Provider = "fakeexternal"
        finding.status_extended = "test"

        output_options = MagicMock()
        output_options.verbose = True
        output_options.status = []
        output_options.fixer = False

        # Clear the global singleton so any unintended fallback would crash.
        previous_global = Provider._global
        Provider._global = None
        try:
            with patch("builtins.print") as mock_print:
                report([finding], local_provider, output_options)

            # report() prints the finding line plus an empty separator when
            # verbose is set; we only care that the finding was rendered using
            # the local provider's `get_stdout_detail` ("fake-detail").
            printed = "".join(
                call.args[0] for call in mock_print.call_args_list if call.args
            )
            assert "fake-detail" in printed
        finally:
            Provider._global = previous_global

    def test_report_sort_calls_get_finding_sort_key(self, fake_provider):
        """Test 29: report else clause calls provider.get_finding_sort_key."""
        from prowler.lib.outputs.outputs import report

        finding1 = MagicMock()
        finding1.status = "PASS"
        finding1.muted = False
        finding1.region = "b-region"
        finding1.check_metadata.Provider = "fakeexternal"
        finding1.status_extended = "test1"

        finding2 = MagicMock()
        finding2.status = "PASS"
        finding2.muted = False
        finding2.region = "a-region"
        finding2.check_metadata.Provider = "fakeexternal"
        finding2.status_extended = "test2"

        output_options = MagicMock()
        output_options.verbose = False
        output_options.status = []

        findings = [finding1, finding2]
        report(findings, fake_provider, output_options)

        # Should be sorted by region (get_finding_sort_key returns "region")
        assert findings[0].region == "a-region"
        assert findings[1].region == "b-region"

    def test_display_summary_table_calls_get_summary_entity(self, fake_provider):
        """Test 30: display_summary_table else clause calls provider.get_summary_entity."""
        from prowler.lib.outputs.summary_table import display_summary_table

        finding = MagicMock()
        finding.status = "FAIL"
        finding.muted = False
        finding.check_metadata.ServiceName = "test_service"
        finding.check_metadata.Provider = "fakeexternal"
        finding.check_metadata.Severity = "high"

        output_options = MagicMock()
        output_options.output_directory = "/tmp"
        output_options.output_filename = "test"
        output_options.output_modes = []

        with patch("builtins.print") as mock_print:
            display_summary_table([finding], fake_provider, output_options)

        printed_text = " ".join(str(c) for c in mock_print.call_args_list)
        assert "Fake Host" in printed_text or "fake-host-1" in printed_text

    def test_generate_output_calls_get_finding_output_data(self, fake_provider):
        """Test 31: finding.generate_output else clause calls provider.get_finding_output_data."""
        from prowler.lib.check.models import (
            CheckMetadata,
            Code,
            Recommendation,
            Remediation,
        )
        from prowler.lib.outputs.finding import Finding

        metadata = CheckMetadata(
            Provider="fakeexternal",
            CheckID="test_check",
            CheckTitle="Test check title",
            CheckType=[],
            ServiceName="test",
            SubServiceName="",
            ResourceIdTemplate="",
            Severity="high",
            ResourceType="Test",
            ResourceGroup="",
            Description="Test description",
            Risk="Test risk",
            RelatedUrl="",
            Remediation=Remediation(
                Code=Code(CLI="", NativeIaC="", Other="", Terraform=""),
                Recommendation=Recommendation(
                    Text="Fix it", Url="https://hub.prowler.com/check/test_check"
                ),
            ),
            Categories=[],
            DependsOn=[],
            RelatedTo=[],
            Notes="",
        )

        check_output = MagicMock()
        check_output.check_metadata = metadata
        check_output.status = "FAIL"
        check_output.status_extended = "test failed"
        check_output.muted = False
        check_output.resource = {}
        check_output.resource_details = ""
        check_output.resource_tags = {}
        check_output.compliance = {}

        output_options = MagicMock()
        output_options.unix_timestamp = False
        output_options.bulk_checks_metadata = {}

        finding = Finding.generate_output(fake_provider, check_output, output_options)

        assert finding.auth_method == "fake"
        assert finding.account_uid == "fake-account"
        assert finding.resource_name == "fake-resource"
        assert finding.region == "local"

    def test_output_options_calls_get_output_options(self, fake_provider):
        """Test 32: __main__.py else clause calls provider.get_output_options."""
        result = fake_provider.get_output_options(MagicMock(), {})

        assert result is not None
        assert hasattr(result, "output_directory")

    def test_html_assessment_calls_get_html_assessment_summary(self, fake_provider):
        """Test 33: html.py fallback calls provider.get_html_assessment_summary."""
        from prowler.lib.outputs.html.html import HTML

        result = HTML.get_assessment_summary(fake_provider)

        assert "<div>Fake Assessment</div>" in result

    def test_compliance_output_calls_generate_compliance_output(self, fake_provider):
        """Test 34: __main__.py else clause calls provider.generate_compliance_output."""
        generated_outputs = {"compliance": []}

        fake_provider.generate_compliance_output(
            [],
            {},
            set(),
            MagicMock(),
            generated_outputs,
        )

        assert "fake-compliance-output" in generated_outputs["compliance"]


# ===========================================================================
# 9. Base Contract Defaults
# ===========================================================================


class TestBaseContractDefaults:
    """Tests for Provider base class default implementations."""

    def test_get_scan_arguments_passes_secret_through(self):
        """Base get_scan_arguments returns the secret unchanged when no mutelist."""
        kwargs = FakeProviderNoHelpText.get_scan_arguments("uid", {"token": "x"})

        assert kwargs == {"token": "x"}

    def test_get_scan_arguments_adds_mutelist_content(self):
        """Base get_scan_arguments adds mutelist_content when provided."""
        kwargs = FakeProviderNoHelpText.get_scan_arguments(
            "uid", {"token": "x"}, {"Mutelist": {}}
        )

        assert kwargs == {"token": "x", "mutelist_content": {"Mutelist": {}}}

    def test_get_scan_arguments_preserves_empty_mutelist_content(self):
        """Base get_scan_arguments passes an explicit empty mutelist through so it
        is not mistaken for an absent mutelist that triggers provider defaults."""
        kwargs = FakeProviderNoHelpText.get_scan_arguments("uid", {"token": "x"}, {})

        assert kwargs == {"token": "x", "mutelist_content": {}}

    def test_get_connection_arguments_passes_secret_through(self):
        """Base get_connection_arguments returns the secret unchanged."""
        kwargs = FakeProviderNoHelpText.get_connection_arguments("uid", {"token": "x"})

        assert kwargs == {"token": "x"}

    def test_get_credentials_schema_defaults_to_empty(self):
        """Base get_credentials_schema declares no schema by default."""
        assert FakeProviderNoHelpText.get_credentials_schema() == {}

    def test_from_cli_args_raises_not_implemented(self):
        """Base Provider.from_cli_args raises NotImplementedError."""
        with pytest.raises(NotImplementedError):
            FakeProviderNoHelpText.from_cli_args(MagicMock(), {})

    def test_get_output_options_raises_not_implemented(self):
        """Base Provider.get_output_options raises NotImplementedError; the
        generic default is applied at the call site via default_output_options."""
        provider = FakeProviderNoHelpText()
        with pytest.raises(NotImplementedError):
            provider.get_output_options(MagicMock(), {})

    def test_default_output_options_builds_generic_default(self):
        """default_output_options returns a generic ProviderOutputOptions so an
        external provider without get_output_options still produces output
        instead of aborting the run."""
        from prowler.config.config import output_file_timestamp
        from prowler.providers.common.models import (
            ProviderOutputOptions,
            default_output_options,
        )

        provider = FakeProviderNoHelpText()
        arguments = Namespace(
            status=None,
            output_formats=None,
            output_directory=None,
            output_filename=None,
            verbose=None,
            only_logs=None,
            unix_timestamp=None,
            shodan=None,
            fixer=None,
        )

        output_options = default_output_options(provider, arguments, {})

        assert isinstance(output_options, ProviderOutputOptions)
        assert (
            output_options.output_filename
            == f"prowler-output-{provider.type}-{output_file_timestamp}"
        )

    def test_default_output_options_honors_explicit_filename(self):
        """A user-supplied output_filename is preserved by default_output_options."""
        from prowler.providers.common.models import default_output_options

        provider = FakeProviderNoHelpText()
        arguments = Namespace(
            status=None,
            output_formats=None,
            output_directory=None,
            output_filename="custom-name",
            verbose=None,
            only_logs=None,
            unix_timestamp=None,
            shodan=None,
            fixer=None,
        )

        output_options = default_output_options(provider, arguments, {})

        assert output_options.output_filename == "custom-name"

    def test_get_stdout_detail_raises_not_implemented(self):
        """Base Provider.get_stdout_detail raises NotImplementedError."""
        provider = FakeProviderNoHelpText()
        with pytest.raises(NotImplementedError):
            provider.get_stdout_detail(MagicMock())

    def test_get_finding_sort_key_returns_none(self):
        """Base Provider.get_finding_sort_key returns None."""
        provider = FakeProviderNoHelpText()
        assert provider.get_finding_sort_key() is None

    def test_get_summary_entity_returns_type_and_account_default(self):
        """Base Provider.get_summary_entity returns (type, account_id) so the
        summary table is not silently dropped for providers that don't override
        it."""
        from types import SimpleNamespace
        from unittest.mock import PropertyMock

        provider = FakeProviderNoHelpText()
        with patch.object(
            type(provider),
            "identity",
            new_callable=PropertyMock,
            return_value=SimpleNamespace(account_id="acc-123"),
        ):
            entity_type, audited_entities = provider.get_summary_entity()

        assert entity_type == provider.type
        assert audited_entities == "acc-123"

    def test_get_summary_entity_defaults_account_to_empty_string(self):
        """When the identity has no account_id, audited_entities falls back to ''."""
        from types import SimpleNamespace
        from unittest.mock import PropertyMock

        provider = FakeProviderNoHelpText()
        with patch.object(
            type(provider),
            "identity",
            new_callable=PropertyMock,
            return_value=SimpleNamespace(),
        ):
            entity_type, audited_entities = provider.get_summary_entity()

        assert entity_type == provider.type
        assert audited_entities == ""

    def test_get_finding_output_data_raises_not_implemented(self):
        """Base Provider.get_finding_output_data raises NotImplementedError."""
        provider = FakeProviderNoHelpText()
        with pytest.raises(NotImplementedError):
            provider.get_finding_output_data(MagicMock())

    def test_get_html_assessment_summary_raises_not_implemented(self):
        """Base Provider.get_html_assessment_summary raises NotImplementedError."""
        provider = FakeProviderNoHelpText()
        with pytest.raises(NotImplementedError):
            provider.get_html_assessment_summary()

    def test_generate_compliance_output_raises_not_implemented(self):
        """Base Provider.generate_compliance_output raises NotImplementedError."""
        provider = FakeProviderNoHelpText()
        with pytest.raises(NotImplementedError):
            provider.generate_compliance_output([], {}, set(), MagicMock(), {})

    def test_get_mutelist_finding_args_raises_not_implemented(self):
        """Base Provider.get_mutelist_finding_args raises NotImplementedError."""
        provider = FakeProviderNoHelpText()
        with pytest.raises(NotImplementedError):
            provider.get_mutelist_finding_args()

    def test_display_compliance_table_raises_not_implemented(self):
        """Base Provider.display_compliance_table raises NotImplementedError."""
        provider = FakeProviderNoHelpText()
        with pytest.raises(NotImplementedError):
            provider.display_compliance_table([], {}, "fw", "out", "/tmp", False)

    def test_is_external_tool_provider_defaults_to_false(self):
        """Base Provider.is_external_tool_provider returns False."""
        provider = FakeProviderNoHelpText()
        assert provider.is_external_tool_provider is False


# ===========================================================================
# 10. Mutelist Dispatch for External Providers
# ===========================================================================


class TestMutelistDispatch:
    """Tests for mutelist integration with external providers."""

    def test_get_mutelist_finding_args_returns_identity(self, fake_provider):
        """External provider returns identity kwargs for mutelist."""
        args = fake_provider.get_mutelist_finding_args()

        assert args == {"host_id": "fake-host-1"}

    def test_mutelist_dispatch_calls_external_provider(self, fake_provider):
        """execute() uses get_mutelist_finding_args for unknown provider types."""
        from prowler.lib.check.check import execute

        # Create a mock check that returns one finding
        finding = MagicMock()
        finding.status = "FAIL"
        finding.muted = False
        finding.check_metadata.Provider = "fakeexternal"

        check = MagicMock()
        check.execute.return_value = [finding]
        check.CheckID = "fake_check"
        check.ServiceName = "fake_service"
        check.Severity.value = "high"

        # Setup mutelist on the provider
        fake_provider.mutelist = MagicMock()
        fake_provider.mutelist.mutelist = {"Accounts": {}}
        fake_provider.mutelist.is_finding_muted.return_value = True

        output_options = MagicMock()
        output_options.status = []
        output_options.unix_timestamp = False

        execute(check, fake_provider, None, output_options)

        # is_finding_muted should have been called with host_id + finding
        fake_provider.mutelist.is_finding_muted.assert_called_once_with(
            host_id="fake-host-1", finding=finding
        )


# ===========================================================================
# 11. Compliance Table Dispatch for External Providers
# ===========================================================================


class TestComplianceTableDispatch:
    """Tests for compliance table display with external providers."""

    def test_display_compliance_table_delegates_to_provider(self, fake_provider):
        """display_compliance_table uses provider method for unknown frameworks."""
        from prowler.lib.outputs.compliance.compliance import (
            display_compliance_table,
        )

        fake_provider.display_compliance_table = MagicMock(return_value=True)

        display_compliance_table(
            [], {}, "custom_1.0_fakeexternal", "out", "/tmp", False
        )

        fake_provider.display_compliance_table.assert_called_once_with(
            [],
            {},
            "custom_1.0_fakeexternal",
            "out",
            "/tmp",
            False,
        )

    def test_display_compliance_table_falls_back_to_generic(self, fake_provider):
        """display_compliance_table falls back to generic when provider returns False."""
        from prowler.lib.outputs.compliance.compliance import (
            display_compliance_table,
        )

        fake_provider.display_compliance_table = MagicMock(return_value=False)

        with patch(
            "prowler.lib.outputs.compliance.compliance.get_generic_compliance_table"
        ) as mock_generic:
            display_compliance_table(
                [], {}, "custom_1.0_fakeexternal", "out", "/tmp", False
            )

        mock_generic.assert_called_once()

    def test_display_compliance_table_falls_back_on_not_implemented(self):
        """display_compliance_table falls back to generic when NotImplementedError."""
        # Use a provider that doesn't implement display_compliance_table
        provider = FakeProviderNoHelpText()
        Provider.set_global_provider(provider)

        with patch(
            "prowler.lib.outputs.compliance.compliance.get_generic_compliance_table"
        ) as mock_generic:
            from prowler.lib.outputs.compliance.compliance import (
                display_compliance_table,
            )

            display_compliance_table(
                [], {}, "unknown_1.0_nohelptext", "out", "/tmp", False
            )

        mock_generic.assert_called_once()
        Provider._global = None


# ===========================================================================
# 12. Provider.get_class — Public side-effect-free class resolver
# ===========================================================================


class TestGetClass:
    """Tests for Provider.get_class(provider) — the public, side-effect-free
    class resolver that unblocks the Django API and other callers that need
    a provider class without triggering CLI side-effects (sys.exit, global
    provider mutation)."""

    # -----------------------------------------------------------------------
    # T1: Built-in provider resolves to correct class
    # -----------------------------------------------------------------------

    def test_get_class_builtin_returns_correct_class(self):
        """get_class('aws') returns AwsProvider — identity check."""
        from prowler.providers.aws.aws_provider import AwsProvider

        cls = Provider.get_class("aws")

        assert cls is AwsProvider

    # -----------------------------------------------------------------------
    # T2: External entry-point provider resolves
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    def test_get_class_external_ep_returns_class(self, mock_is_builtin, mock_ep):
        """get_class resolves an external entry-point provider and returns that class."""
        mock_is_builtin.return_value = False
        mock_ep.return_value = [
            _make_entry_point(
                "fakeexternal", "pkg:FakeExternalProvider", "prowler.providers"
            ),
        ]
        mock_ep.return_value[0].load.return_value = FakeExternalProvider

        cls = Provider.get_class("fakeexternal")

        assert cls is FakeExternalProvider

    # -----------------------------------------------------------------------
    # T3: Unknown provider raises, does NOT call sys.exit
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    def test_get_class_unknown_raises_and_does_not_sys_exit(
        self, mock_is_builtin, mock_ep
    ):
        """get_class raises for an unknown provider and never calls sys.exit."""
        mock_is_builtin.return_value = False
        mock_ep.return_value = []

        # Assert ImportError specifically to enforce the public API contract
        # (not a broad Exception). SystemExit belongs in init_global_provider's
        # wrapper, not in the pure resolver.
        with pytest.raises(ImportError):
            Provider.get_class("totally_unknown_xyz_provider")

    # -----------------------------------------------------------------------
    # T4: get_class is PURE for built-ins — no collision warning, no EP call
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.logger")
    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.import_module")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    def test_get_class_builtin_with_ep_shadow_is_pure(
        self, mock_is_builtin, mock_import, mock_load_ep, mock_logger
    ):
        """get_class for a built-in with a same-named EP is PURE:
        - returns the built-in class
        - does NOT emit a collision warning
        - does NOT call _load_ep_provider (so _ep_providers cache stays empty for
          this key, proving no side-effect)
        """
        import types

        mock_is_builtin.return_value = True
        mock_load_ep.return_value = FakeExternalProvider  # plug-in shadow present

        fake_module = types.ModuleType("fake_builtin_module")
        fake_builtin_cls = type("AwsProvider", (Provider,), {"_type": "aws"})
        fake_module.AwsProvider = fake_builtin_cls
        mock_import.return_value = fake_module

        cls = Provider.get_class("aws")

        # Built-in class returned
        assert cls is fake_builtin_cls
        # No collision warning emitted — that is now init_global_provider's job
        warning_msgs = [
            call.args[0]
            for call in mock_logger.warning.call_args_list
            if call.args and "IGNORED" in call.args[0]
        ]
        assert not warning_msgs, (
            "get_class must NOT emit a collision warning; "
            "init_global_provider owns that responsibility"
        )
        # _load_ep_provider must NOT have been called for the built-in path
        mock_load_ep.assert_not_called()
        # _ep_providers cache must not contain 'aws' (no side-effect)
        assert "aws" not in Provider._ep_providers

    # -----------------------------------------------------------------------
    # T4b: Built-in module missing its expected class raises ImportError
    #      and does NOT fall back to a same-named entry point
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.import_module")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    def test_get_class_builtin_missing_class_raises_importerror(
        self, mock_is_builtin, mock_import, mock_load_ep
    ):
        """When is_builtin is True but the module does not define the expected
        provider class, get_class raises ImportError and does NOT fall back to a
        same-named entry point — falling back would contradict is_builtin and
        silently return a foreign class."""
        import types

        mock_is_builtin.return_value = True
        # Module imports fine but lacks the expected `<Name>Provider` attribute.
        empty_module = types.ModuleType("empty_builtin_module")
        mock_import.return_value = empty_module

        with pytest.raises(ImportError):
            Provider.get_class("aws")

        # Must NOT fall back to entry points for a (broken) built-in.
        mock_load_ep.assert_not_called()

    # -----------------------------------------------------------------------
    # T4c: Entry point resolving to a non-Provider class raises ImportError
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    def test_get_class_external_ep_not_provider_subclass_raises_importerror(
        self, mock_is_builtin, mock_ep
    ):
        """When an entry point resolves to an object that is not a Provider
        subclass, get_class raises ImportError instead of returning it, so the
        public contract (a Provider subclass) is enforced rather than trusted."""

        class NotAProvider:
            pass

        mock_is_builtin.return_value = False
        mock_ep.return_value = [
            _make_entry_point("rogue", "pkg:NotAProvider", "prowler.providers"),
        ]
        mock_ep.return_value[0].load.return_value = NotAProvider

        with pytest.raises(ImportError):
            Provider.get_class("rogue")

    # -----------------------------------------------------------------------
    # T4d: Contract — every built-in provider stays resolvable via get_class
    # -----------------------------------------------------------------------

    @pytest.mark.parametrize(
        "provider",
        [
            name
            for name in Provider.get_available_providers()
            if Provider.is_builtin(name)
        ],
    )
    def test_get_class_resolves_every_builtin_provider(self, provider):
        """Contract test over all built-in providers: each one must remain
        resolvable through get_class and return a Provider subclass whose name
        follows the `{Capitalized}Provider` convention. This pins the naming
        convention as the built-in resolution contract, so a future provider
        that breaks it fails here instead of silently at runtime in a caller
        (e.g. the API)."""
        cls = Provider.get_class(provider)

        assert isinstance(cls, type) and issubclass(cls, Provider)
        assert cls.__name__ == f"{provider.capitalize()}Provider"

    # -----------------------------------------------------------------------
    # T5: Regression — init_global_provider still resolves external correctly
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.load_and_validate_config_file")
    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    def test_init_global_provider_still_resolves_external_via_get_class(
        self, mock_load_ep, mock_config
    ):
        """Regression: init_global_provider continues to work for external providers
        after the class-resolution block is delegated to get_class.

        'fakepure' is not a built-in, so is_builtin() returns False and get_class
        takes the entry-point path. This verifies the FakePureContractProvider path
        (pure from_cli_args returning an instance) still works — i.e.,
        init_global_provider correctly wires the returned instance as global provider.
        """
        mock_load_ep.return_value = FakePureContractProvider
        mock_config.return_value = {}

        args = Namespace(
            provider="fakepure",
            fixer_config="config.yaml",
            config_file="config.yaml",
        )

        Provider._global = None
        Provider.init_global_provider(args)

        assert isinstance(Provider._global, FakePureContractProvider)
        Provider._global = None

    # -----------------------------------------------------------------------
    # T6: Regression — get_providers_help_text returns same text after refactor
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.Provider._load_ep_provider")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_providers_help_text_identical_after_refactor_external(
        self, mock_providers, mock_load_ep
    ):
        """get_providers_help_text returns identical _cli_help_text for an external
        provider both before and after the refactor to use get_class internally."""
        mock_providers.return_value = ["fakeexternal"]
        mock_load_ep.return_value = FakeExternalProvider

        help_text = Provider.get_providers_help_text()

        # Must match the known _cli_help_text on FakeExternalProvider
        assert help_text["fakeexternal"] == "Fake External Provider"

    @patch("prowler.providers.common.provider.import_module")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    @patch("prowler.providers.common.provider.Provider.get_available_providers")
    def test_get_providers_help_text_identical_after_refactor_builtin(
        self, mock_providers, mock_is_builtin, mock_import
    ):
        """get_providers_help_text returns identical _cli_help_text for a built-in
        provider both before and after the refactor to use get_class internally.
        is_builtin is mocked to True so get_class takes the built-in import path."""
        import types

        mock_providers.return_value = ["fakebuiltin"]
        mock_is_builtin.return_value = True
        mock_cls = type(
            "FakebuiltinProvider", (Provider,), {"_cli_help_text": "Built-in Help"}
        )
        mock_module = types.ModuleType("fake_module")
        mock_module.FakebuiltinProvider = mock_cls
        mock_import.return_value = mock_module

        help_text = Provider.get_providers_help_text()

        assert help_text["fakebuiltin"] == "Built-in Help"

    # -----------------------------------------------------------------------
    # T7: init_global_provider emits collision warning (not get_class)
    # -----------------------------------------------------------------------

    @patch("prowler.providers.common.provider.load_and_validate_config_file")
    @patch("prowler.providers.common.provider.importlib.metadata.entry_points")
    @patch("prowler.providers.common.provider.import_module")
    @patch("prowler.providers.common.provider.Provider.is_builtin")
    def test_init_global_provider_emits_collision_warning_for_builtin_ep_shadow(
        self, mock_is_builtin, mock_import, mock_entry_points, mock_config, caplog
    ):
        """init_global_provider (not get_class) emits the collision warning
        when a built-in provider has a same-named entry-point plug-in registered.

        This is the counterpart to test_get_class_builtin_with_ep_shadow_is_pure:
        the warning responsibility moved OUT of get_class and INTO
        init_global_provider, so users still see the message on CLI invocation
        but prowler --help and API calls (which never hit init_global_provider)
        do not spuriously emit it. The shadow is detected by entry-point name
        only — the plug-in is never loaded to warn.
        """
        import logging
        import types

        mock_is_builtin.return_value = True
        shadow_ep = MagicMock()
        shadow_ep.name = "aws"  # plug-in shadowing the built-in name
        mock_entry_points.return_value = [shadow_ep]

        fake_module = types.ModuleType("fake_builtin_module")
        fake_module.AwsProvider = MagicMock(side_effect=lambda **_kw: None)
        mock_import.return_value = fake_module
        mock_config.return_value = {}

        args = Namespace(
            provider="aws",
            fixer_config="config.yaml",
            config_file="config.yaml",
            aws_retries_max_attempts=3,
            role=None,
            session_duration=None,
            external_id=None,
            role_session_name=None,
            mfa=None,
            profile=None,
            region=None,
            excluded_region=None,
            organizations_role=None,
            scan_unused_services=False,
            resource_tag=None,
            resource_arn=None,
            mutelist_file=None,
        )

        Provider._global = None
        with caplog.at_level(logging.WARNING, logger="prowler"):
            try:
                Provider.init_global_provider(args)
            except BaseException:
                # AwsProvider mock is fake; dispatch may fail — only the
                # warning emitted BEFORE dispatch matters here.
                pass
        Provider._global = None

        collision_warnings = [
            r.message
            for r in caplog.records
            if "Plug-in provider 'aws'" in r.message and "IGNORED" in r.message
        ]
        assert collision_warnings, (
            "init_global_provider must emit the collision warning when a "
            "same-named EP plug-in exists for a built-in provider"
        )
        # Shadow detected by name only — the plug-in is never loaded to warn.
        shadow_ep.load.assert_not_called()
