import json
import os
import tempfile
from unittest.mock import MagicMock, patch

import pytest
from pydantic.v1 import ValidationError

from prowler.lib.check.compliance_models import (
    AttributeMetadata,
    ChartConfig,
    Compliance,
    ComplianceFramework,
    CriticalRequirementsFilter,
    EnumValueDisplay,
    I18nLabels,
    OutputFormats,
    OutputsConfig,
    PDFConfig,
    ReportFilter,
    ScoringConfig,
    ScoringFormula,
    SplitByConfig,
    TableConfig,
    TableLabels,
    UniversalComplianceRequirement,
    adapt_legacy_to_universal,
    get_bulk_compliance_frameworks_universal,
    load_compliance_framework_universal,
)
from tests.lib.outputs.compliance.fixtures import (
    CIS_1_4_AWS,
    ENS_RD2022_AWS,
    KISA_ISMSP_AWS,
    MITRE_ATTACK_AWS,
    NIST_800_53_REVISION_4_AWS,
    PROWLER_THREATSCORE_AWS,
)


class TestOutputFormats:
    def test_defaults(self):
        of = OutputFormats()
        assert of.csv is True
        assert of.ocsf is True

    def test_explicit_false(self):
        of = OutputFormats(csv=False, ocsf=False)
        assert of.csv is False
        assert of.ocsf is False


class TestAttributeMetadata:
    def test_basic(self):
        meta = AttributeMetadata(key="Section", type="str")
        assert meta.key == "Section"
        assert meta.type == "str"
        assert meta.output_formats.csv is True
        assert meta.required is False

    def test_with_enum(self):
        meta = AttributeMetadata(
            key="Profile",
            type="str",
            enum=["Level 1", "Level 2"],
        )
        assert meta.enum == ["Level 1", "Level 2"]

    def test_int_type(self):
        meta = AttributeMetadata(key="LevelOfRisk", type="int", required=True)
        assert meta.type == "int"
        assert meta.required is True

    def test_enum_display_field(self):
        meta = AttributeMetadata(
            key="Dimensiones",
            type="str",
            enum=["confidencialidad", "integridad", "trazabilidad"],
            enum_display={
                "confidencialidad": {
                    "label": "Confidencialidad",
                    "abbreviation": "C",
                    "color": "#FF6347",
                },
                "integridad": {
                    "label": "Integridad",
                    "abbreviation": "I",
                    "color": "#4286F4",
                },
                "trazabilidad": {
                    "label": "Trazabilidad",
                    "abbreviation": "T",
                    "color": "#32CD32",
                },
            },
        )
        assert meta.enum_display is not None
        assert meta.enum_display["confidencialidad"]["abbreviation"] == "C"
        assert meta.enum_display["integridad"]["color"] == "#4286F4"

    def test_enum_order_field(self):
        meta = AttributeMetadata(
            key="Nivel",
            type="str",
            enum=["opcional", "bajo", "medio", "alto"],
            enum_order=["alto", "medio", "bajo", "opcional"],
        )
        assert meta.enum_order == ["alto", "medio", "bajo", "opcional"]

    def test_chart_label_field(self):
        meta = AttributeMetadata(
            key="Section",
            type="str",
            chart_label="Security Domain",
        )
        assert meta.chart_label == "Security Domain"

    def test_output_formats_default_true(self):
        meta = AttributeMetadata(key="Section")
        assert meta.output_formats.csv is True
        assert meta.output_formats.ocsf is True

    def test_output_formats_explicit_false(self):
        meta = AttributeMetadata(
            key="InternalNote",
            output_formats=OutputFormats(csv=False, ocsf=False),
        )
        assert meta.output_formats.csv is False
        assert meta.output_formats.ocsf is False

    def test_new_fields_default_none(self):
        meta = AttributeMetadata(key="Section")
        assert meta.enum_display is None
        assert meta.enum_order is None
        assert meta.chart_label is None


class TestEnumValueDisplay:
    def test_basic(self):
        evd = EnumValueDisplay(label="Test")
        assert evd.label == "Test"
        assert evd.abbreviation is None
        assert evd.color is None
        assert evd.icon is None

    def test_dimension_style(self):
        evd = EnumValueDisplay(
            label="Trazabilidad",
            abbreviation="T",
            color="#4286F4",
        )
        assert evd.label == "Trazabilidad"
        assert evd.abbreviation == "T"
        assert evd.color == "#4286F4"

    def test_tipo_style(self):
        evd = EnumValueDisplay(
            label="Requisito",
            icon="⚠️",
        )
        assert evd.icon == "⚠️"
        assert evd.abbreviation is None


class TestChartConfig:
    def test_horizontal_bar(self):
        chart = ChartConfig(
            id="section_compliance",
            type="horizontal_bar",
            group_by="Section",
            title="Compliance Score by Domain",
            y_label="Domain",
            x_label="Compliance %",
        )
        assert chart.type == "horizontal_bar"
        assert chart.group_by == "Section"
        assert chart.value_source == "compliance_percent"
        assert chart.color_mode == "by_value"

    def test_vertical_bar(self):
        chart = ChartConfig(
            id="risk_distribution",
            type="vertical_bar",
            group_by="LevelOfRisk",
            color_mode="fixed",
            fixed_color="#336699",
        )
        assert chart.type == "vertical_bar"
        assert chart.fixed_color == "#336699"

    def test_radar(self):
        chart = ChartConfig(
            id="dimension_radar",
            type="radar",
            group_by="Dimensiones",
        )
        assert chart.type == "radar"

    def test_defaults(self):
        chart = ChartConfig(id="test", type="vertical_bar", group_by="Section")
        assert chart.title is None
        assert chart.x_label is None
        assert chart.y_label is None
        assert chart.value_source == "compliance_percent"
        assert chart.color_mode == "by_value"
        assert chart.fixed_color is None


class TestScoringFormula:
    def test_threatscore_style(self):
        formula = ScoringFormula(
            risk_field="LevelOfRisk",
            weight_field="Weight",
            risk_boost_factor=0.25,
        )
        assert formula.risk_field == "LevelOfRisk"
        assert formula.weight_field == "Weight"
        assert formula.risk_boost_factor == 0.25

    def test_custom_boost_factor(self):
        formula = ScoringFormula(
            risk_field="Risk",
            weight_field="Impact",
            risk_boost_factor=0.5,
        )
        assert formula.risk_boost_factor == 0.5

    def test_default_boost_factor(self):
        formula = ScoringFormula(risk_field="LevelOfRisk", weight_field="Weight")
        assert formula.risk_boost_factor == 0.25


class TestCriticalRequirementsFilter:
    def test_int_based(self):
        crf = CriticalRequirementsFilter(
            filter_field="LevelOfRisk",
            min_value=4,
            title="Critical Failed Requirements",
        )
        assert crf.filter_field == "LevelOfRisk"
        assert crf.min_value == 4
        assert crf.filter_value is None
        assert crf.status_filter == "FAIL"
        assert crf.title == "Critical Failed Requirements"

    def test_string_based(self):
        crf = CriticalRequirementsFilter(
            filter_field="Nivel",
            filter_value="alto",
        )
        assert crf.filter_value == "alto"
        assert crf.min_value is None

    def test_defaults(self):
        crf = CriticalRequirementsFilter(filter_field="LevelOfRisk")
        assert crf.status_filter == "FAIL"
        assert crf.title is None
        assert crf.min_value is None
        assert crf.filter_value is None


class TestReportFilter:
    def test_defaults(self):
        rf = ReportFilter()
        assert rf.only_failed is True
        assert rf.include_manual is False

    def test_custom(self):
        rf = ReportFilter(only_failed=False, include_manual=True)
        assert rf.only_failed is False
        assert rf.include_manual is True


class TestI18nLabels:
    def test_english_defaults(self):
        labels = I18nLabels()
        assert labels.page_label == "Page"
        assert labels.powered_by == "Powered by Prowler"
        assert labels.framework_label == "Framework:"
        assert labels.provider_label == "Provider:"
        assert labels.report_title is None

    def test_spanish_override(self):
        labels = I18nLabels(
            report_title="Informe de Cumplimiento ENS",
            page_label="Página",
            powered_by="Generado por Prowler",
            framework_label="Marco:",
            version_label="Versión:",
            provider_label="Proveedor:",
            description_label="Descripción:",
            compliance_score_label="Puntuación de Cumplimiento por Secciones",
            requirements_index_label="Índice de Requisitos",
            detailed_findings_label="Hallazgos Detallados",
        )
        assert labels.page_label == "Página"
        assert labels.provider_label == "Proveedor:"
        assert labels.report_title == "Informe de Cumplimiento ENS"


class TestSplitByConfig:
    def test_cis_style(self):
        config = SplitByConfig(field="Profile", values=["Level 1", "Level 2"])
        assert config.field == "Profile"
        assert len(config.values) == 2

    def test_ens_style(self):
        config = SplitByConfig(
            field="Nivel",
            values=["alto", "medio", "bajo", "opcional"],
        )
        assert len(config.values) == 4


class TestScoringConfig:
    def test_threatscore_style(self):
        config = ScoringConfig(risk_field="LevelOfRisk", weight_field="Weight")
        assert config.risk_field == "LevelOfRisk"
        assert config.weight_field == "Weight"


class TestTableLabels:
    def test_defaults(self):
        labels = TableLabels()
        assert labels.pass_label == "PASS"
        assert labels.fail_label == "FAIL"
        assert labels.provider_header == "Provider"

    def test_ens_spanish(self):
        labels = TableLabels(
            pass_label="CUMPLE",
            fail_label="NO CUMPLE",
            provider_header="Proveedor",
        )
        assert labels.pass_label == "CUMPLE"


class TestTableConfig:
    def test_grouped_mode(self):
        tc = TableConfig(group_by="Section")
        assert tc.group_by == "Section"
        assert tc.split_by is None
        assert tc.scoring is None

    def test_split_mode(self):
        tc = TableConfig(
            group_by="Section",
            split_by=SplitByConfig(field="Profile", values=["Level 1", "Level 2"]),
        )
        assert tc.split_by is not None
        assert tc.split_by.field == "Profile"

    def test_scored_mode(self):
        tc = TableConfig(
            group_by="Section",
            scoring=ScoringConfig(risk_field="LevelOfRisk", weight_field="Weight"),
        )
        assert tc.scoring is not None


class TestPDFConfig:
    def test_defaults(self):
        pdf = PDFConfig()
        assert pdf.language == "en"
        assert pdf.logo_filename is None
        assert pdf.primary_color is None
        assert pdf.sections is None
        assert pdf.section_short_names is None
        assert pdf.group_by_field is None
        assert pdf.sub_group_by_field is None
        assert pdf.section_titles is None
        assert pdf.charts is None
        assert pdf.scoring is None
        assert pdf.critical_filter is None
        assert pdf.filter is None
        assert pdf.labels is None

    def test_csa_ccm_style(self):
        pdf = PDFConfig(
            primary_color="#336699",
            secondary_color="#4D80B3",
            bg_color="#F2F8FF",
            group_by_field="Section",
            sections=["Audit & Assurance", "Identity & Access Management"],
            section_short_names={"Identity & Access Management": "IAM"},
            charts=[
                ChartConfig(
                    id="section_compliance",
                    type="horizontal_bar",
                    group_by="Section",
                    title="Compliance Score by Domain",
                ).dict()
            ],
            filter=ReportFilter(only_failed=True, include_manual=False),
        )
        assert pdf.primary_color == "#336699"
        assert len(pdf.sections) == 2
        assert pdf.section_short_names["Identity & Access Management"] == "IAM"
        assert pdf.group_by_field == "Section"
        assert pdf.charts is not None
        assert len(pdf.charts) == 1
        assert pdf.filter.only_failed is True

    def test_ens_style(self):
        pdf = PDFConfig(
            language="es",
            logo_filename="ens_logo.png",
            primary_color="#CC3333",
            group_by_field="Marco",
            sub_group_by_field="Categoria",
            labels=I18nLabels(
                page_label="Página",
                provider_label="Proveedor:",
            ),
        )
        assert pdf.language == "es"
        assert pdf.logo_filename == "ens_logo.png"
        assert pdf.group_by_field == "Marco"
        assert pdf.sub_group_by_field == "Categoria"
        assert pdf.labels.page_label == "Página"

    def test_threatscore_style(self):
        pdf = PDFConfig(
            primary_color="#336699",
            sections=["1. IAM", "2. Attack Surface"],
            scoring=ScoringFormula(
                risk_field="LevelOfRisk",
                weight_field="Weight",
                risk_boost_factor=0.25,
            ),
            critical_filter=CriticalRequirementsFilter(
                filter_field="LevelOfRisk",
                min_value=4,
                title="Critical Failed Requirements",
            ),
        )
        assert pdf.scoring is not None
        assert pdf.scoring.risk_field == "LevelOfRisk"
        assert pdf.critical_filter.min_value == 4

    def test_section_titles(self):
        pdf = PDFConfig(
            section_titles={
                "1": "1. Policy on Security",
                "2": "2. Risk Management",
            },
        )
        assert pdf.section_titles["1"] == "1. Policy on Security"

    def test_in_framework(self):
        fw = ComplianceFramework(
            framework="Test",
            name="Test Framework",
            description="Test",
            requirements=[],
            outputs=OutputsConfig(
                pdf_config=PDFConfig(
                    primary_color="#336699",
                    sections=["Section A"],
                    charts=[
                        ChartConfig(
                            id="test_chart",
                            type="vertical_bar",
                            group_by="Section",
                        ).dict()
                    ],
                ),
            ),
        )
        assert fw.outputs is not None
        assert fw.outputs.pdf_config is not None
        assert fw.outputs.pdf_config.primary_color == "#336699"
        assert fw.outputs.pdf_config.sections == ["Section A"]
        assert fw.outputs.pdf_config.charts is not None
        assert len(fw.outputs.pdf_config.charts) == 1
        assert fw.outputs.pdf_config.charts[0]["id"] == "test_chart"
        assert fw.outputs.pdf_config.charts[0]["type"] == "vertical_bar"

    def test_framework_without_pdf_config(self):
        fw = ComplianceFramework(
            framework="Test",
            name="Test Framework",
            description="Test",
            requirements=[],
        )
        assert fw.outputs is None


class TestUniversalComplianceRequirement:
    def test_flat_dict_attributes(self):
        req = UniversalComplianceRequirement(
            id="1.1",
            description="Test requirement",
            attributes={"Section": "IAM", "Profile": "Level 1"},
            checks={"aws": ["check_a", "check_b"]},
        )
        assert req.attributes["Section"] == "IAM"
        assert len(req.checks["aws"]) == 2

    def test_mitre_optional_fields(self):
        req = UniversalComplianceRequirement(
            id="T1190",
            description="Exploit Public-Facing Application",
            attributes={},
            checks={"aws": ["drs_job_exist"]},
            tactics=["Initial Access"],
            sub_techniques=[],
            platforms=["IaaS", "Linux"],
            technique_url="https://attack.mitre.org/techniques/T1190/",
        )
        assert req.tactics == ["Initial Access"]
        assert req.technique_url == "https://attack.mitre.org/techniques/T1190/"

    def test_dict_checks_multi_provider(self):
        req = UniversalComplianceRequirement(
            id="1.1",
            description="Multi-provider",
            attributes={},
            checks={"aws": ["check_a"], "azure": ["check_b"]},
        )
        assert isinstance(req.checks, dict)
        assert "aws" in req.checks

    def test_empty_checks(self):
        req = UniversalComplianceRequirement(
            id="manual-1",
            description="Manual requirement",
            attributes={"Section": "Governance"},
            checks={},
        )
        assert req.checks == {}

    def test_checks_default_is_empty_dict(self):
        req = UniversalComplianceRequirement(
            id="1.1",
            description="No checks provided",
        )
        assert req.checks == {}


class TestComplianceFramework:
    def test_basic_framework(self):
        fw = ComplianceFramework(
            framework="TestFW",
            name="Test Framework",
            provider="AWS",
            version="1.0",
            description="A test framework",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="Test",
                    attributes={"Section": "IAM"},
                    checks={"aws": ["check_a"]},
                )
            ],
            attributes_metadata=[
                AttributeMetadata(key="Section", type="str"),
            ],
            outputs=OutputsConfig(table_config=TableConfig(group_by="Section")),
        )
        assert fw.framework == "TestFW"
        assert fw.outputs.table_config.group_by == "Section"
        assert len(fw.attributes_metadata) == 1
        assert len(fw.requirements) == 1

    def test_optional_provider(self):
        fw = ComplianceFramework(
            framework="MultiCloud",
            name="Multi-cloud framework",
            description="A multi-provider framework",
            requirements=[],
        )
        assert fw.provider is None

    def test_get_providers_from_dict_checks(self):
        fw = ComplianceFramework(
            framework="MultiCloud",
            name="Multi-cloud",
            description="test",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="test",
                    attributes={},
                    checks={
                        "aws": ["check_a"],
                        "azure": ["check_b"],
                        "gcp": ["check_c"],
                    },
                ),
                UniversalComplianceRequirement(
                    id="1.2",
                    description="test2",
                    attributes={},
                    checks={"aws": ["check_d"]},
                ),
            ],
        )
        providers = fw.get_providers()
        assert providers == ["aws", "azure", "gcp"]

    def test_get_providers_fallback_to_explicit(self):
        fw = ComplianceFramework(
            framework="SingleCloud",
            name="Single-cloud",
            provider="AWS",
            description="test",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="test",
                    attributes={},
                    checks={},
                ),
            ],
        )
        providers = fw.get_providers()
        assert providers == ["aws"]

    def test_supports_provider_dict_checks(self):
        fw = ComplianceFramework(
            framework="MultiCloud",
            name="Multi-cloud",
            description="test",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="test",
                    attributes={},
                    checks={"aws": ["check_a"], "azure": ["check_b"]},
                ),
            ],
        )
        assert fw.supports_provider("aws") is True
        assert fw.supports_provider("azure") is True
        assert fw.supports_provider("gcp") is False

    def test_supports_provider_explicit_only(self):
        """Framework with explicit provider but no per-requirement checks still supports the provider."""
        fw = ComplianceFramework(
            framework="SingleCloud",
            name="Single-cloud",
            provider="AWS",
            description="test",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="Manual requirement",
                    attributes={},
                    checks={},
                ),
            ],
        )
        assert fw.supports_provider("aws") is True
        assert fw.supports_provider("azure") is False

    def test_no_provider_field_with_dict_checks(self):
        """Multi-provider JSON has no Provider field — providers derived from checks."""
        fw = ComplianceFramework(
            framework="CSA_CCM",
            name="CSA CCM 4.0",
            description="Cloud Controls Matrix",
            requirements=[
                UniversalComplianceRequirement(
                    id="A&A-01",
                    description="Audit & Assurance",
                    attributes={"Domain": "A&A"},
                    checks={
                        "aws": ["check_a"],
                        "azure": ["check_b"],
                        "gcp": ["check_c"],
                    },
                ),
            ],
        )
        assert fw.provider is None
        assert fw.get_providers() == ["aws", "azure", "gcp"]
        assert fw.supports_provider("aws")
        assert fw.supports_provider("azure")
        assert fw.supports_provider("gcp")
        assert not fw.supports_provider("kubernetes")

    def test_icon_field(self):
        fw = ComplianceFramework(
            framework="CSA_CCM",
            name="CSA CCM 4.0",
            description="Cloud Controls Matrix",
            icon="csa",
            requirements=[],
        )
        assert fw.icon == "csa"

    def test_icon_defaults_to_none(self):
        fw = ComplianceFramework(
            framework="Test",
            name="Test",
            description="d",
            requirements=[],
        )
        assert fw.icon is None


class TestAdaptLegacyToUniversal:
    def test_adapt_cis(self):
        fw = adapt_legacy_to_universal(CIS_1_4_AWS)
        assert fw.framework == "CIS"
        assert fw.provider == "AWS"
        assert len(fw.requirements) == 2
        # First requirement should have flat attributes
        req = fw.requirements[0]
        assert "Section" in req.attributes
        assert req.attributes["Section"] == "2. Storage"
        assert req.tactics is None
        # Checks must be wrapped in dict keyed by provider
        assert isinstance(req.checks, dict)
        assert "aws" in req.checks

    def test_adapt_ens(self):
        fw = adapt_legacy_to_universal(ENS_RD2022_AWS)
        assert fw.framework == "ENS"
        req = fw.requirements[0]
        assert "Marco" in req.attributes
        assert req.attributes["Marco"] == "operacional"

    def test_adapt_mitre(self):
        fw = adapt_legacy_to_universal(MITRE_ATTACK_AWS)
        assert fw.framework == "MITRE-ATTACK"
        req = fw.requirements[0]
        assert req.tactics == ["Initial Access"]
        assert req.technique_url == "https://attack.mitre.org/techniques/T1190/"
        assert "_raw_attributes" in req.attributes
        assert isinstance(req.checks, dict)
        assert "aws" in req.checks

    def test_adapt_threatscore(self):
        fw = adapt_legacy_to_universal(PROWLER_THREATSCORE_AWS)
        req = fw.requirements[0]
        assert req.attributes["LevelOfRisk"] == 5
        assert req.attributes["Weight"] == 1000

    def test_adapt_generic(self):
        fw = adapt_legacy_to_universal(NIST_800_53_REVISION_4_AWS)
        req = fw.requirements[0]
        assert "Section" in req.attributes

    def test_adapt_kisa(self):
        fw = adapt_legacy_to_universal(KISA_ISMSP_AWS)
        req = fw.requirements[0]
        assert "Domain" in req.attributes

    def test_inferred_metadata_cis(self):
        fw = adapt_legacy_to_universal(CIS_1_4_AWS)
        assert fw.attributes_metadata is not None
        keys = [m.key for m in fw.attributes_metadata]
        assert "Section" in keys
        assert "Profile" in keys

    def test_inferred_metadata_mitre_is_none(self):
        fw = adapt_legacy_to_universal(MITRE_ATTACK_AWS)
        assert fw.attributes_metadata is None

    def test_table_config_is_none(self):
        fw = adapt_legacy_to_universal(CIS_1_4_AWS)
        assert fw.outputs is None


class TestLoadComplianceFrameworkUniversal:
    def test_load_universal_format(self, tmp_path):
        data = {
            "framework": "TestFW",
            "name": "Test",
            "provider": "AWS",
            "version": "1.0",
            "description": "desc",
            "icon": "prowlerthreatscore",
            "attributes_metadata": [{"key": "Section", "type": "str"}],
            "outputs": {"table_config": {"group_by": "Section"}},
            "requirements": [
                {
                    "id": "1.1",
                    "description": "test",
                    "attributes": {"Section": "IAM"},
                    "checks": {"aws": ["check_a"]},
                }
            ],
        }
        path = tmp_path / "test.json"
        path.write_text(json.dumps(data))
        fw = load_compliance_framework_universal(str(path))
        assert fw is not None
        assert fw.framework == "TestFW"
        assert fw.icon == "prowlerthreatscore"
        assert fw.outputs.table_config.group_by == "Section"

    def test_load_universal_multi_provider(self, tmp_path):
        data = {
            "framework": "CSA_CCM",
            "name": "CSA CCM 4.0",
            "version": "4.0",
            "description": "Cloud Controls Matrix",
            "attributes_metadata": [{"key": "Domain", "type": "str"}],
            "outputs": {"table_config": {"group_by": "Domain"}},
            "requirements": [
                {
                    "id": "A&A-01",
                    "description": "Audit",
                    "attributes": {"Domain": "Audit"},
                    "checks": {
                        "aws": ["check_a"],
                        "azure": ["check_b"],
                        "gcp": ["check_c"],
                    },
                }
            ],
        }
        path = tmp_path / "csa_ccm_4.0.json"
        path.write_text(json.dumps(data))
        fw = load_compliance_framework_universal(str(path))
        assert fw is not None
        assert fw.provider is None
        assert fw.get_providers() == ["aws", "azure", "gcp"]
        assert fw.supports_provider("aws")
        assert not fw.supports_provider("kubernetes")

    def test_load_legacy_format(self, tmp_path):
        data = {
            "Framework": "SOC2",
            "Name": "SOC2",
            "Provider": "AWS",
            "Version": "",
            "Description": "desc",
            "Requirements": [
                {
                    "Id": "1.1",
                    "Description": "test",
                    "Attributes": [{"Section": "Access Control"}],
                    "Checks": ["check_a"],
                }
            ],
        }
        path = tmp_path / "legacy.json"
        path.write_text(json.dumps(data))
        fw = load_compliance_framework_universal(str(path))
        assert fw is not None
        assert fw.framework == "SOC2"
        assert fw.outputs is None
        assert fw.requirements[0].attributes["Section"] == "Access Control"
        assert fw.requirements[0].checks == {"aws": ["check_a"]}


class TestSmokeLoadAllJSONs:
    """Parametrized smoke test: every existing compliance JSON must load as ComplianceFramework."""

    @staticmethod
    def _find_all_compliance_jsons():
        base = os.path.join(
            os.path.dirname(__file__),
            "..",
            "..",
            "..",
            "prowler",
            "compliance",
        )
        base = os.path.normpath(base)
        jsons = []
        if os.path.isdir(base):
            # Top-level JSONs (multi-provider)
            for filename in os.listdir(base):
                if filename.endswith(".json"):
                    jsons.append(os.path.join(base, filename))
            # Provider sub-directory JSONs
            for provider_dir in os.listdir(base):
                provider_path = os.path.join(base, provider_dir)
                if os.path.isdir(provider_path):
                    for filename in os.listdir(provider_path):
                        if filename.endswith(".json"):
                            jsons.append(os.path.join(provider_path, filename))
        return jsons

    @pytest.mark.parametrize(
        "json_path",
        _find_all_compliance_jsons.__func__(),
        ids=lambda p: os.path.basename(p),
    )
    def test_loads_as_universal(self, json_path):
        fw = load_compliance_framework_universal(json_path)
        assert fw is not None, f"Failed to load {json_path}"
        assert fw.framework
        assert fw.name
        assert len(fw.requirements) >= 0


class TestBackwardCompat:
    """Ensure Compliance.get_bulk still returns Compliance objects."""

    def test_get_bulk_still_works(self):
        # This test just validates the legacy path still returns Compliance objects
        # We test with a constructed Compliance object
        legacy = CIS_1_4_AWS
        assert isinstance(legacy, Compliance)
        assert legacy.Framework == "CIS"


class TestAttributesMetadataValidation:
    """Validate that Requirement attributes match their attributes_metadata schema."""

    def _metadata(self, required=False, enum=None, type_str="str"):
        return [
            AttributeMetadata(key="Section", type="str", required=True),
            AttributeMetadata(key="Level", type=type_str, required=required, enum=enum),
        ]

    def test_valid_attributes_pass(self):
        fw = ComplianceFramework(
            framework="Test",
            name="Test",
            description="d",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="d",
                    attributes={"Section": "IAM", "Level": "high"},
                    checks={},
                ),
            ],
            attributes_metadata=self._metadata(),
        )
        assert len(fw.requirements) == 1

    def test_missing_required_key_raises(self):
        with pytest.raises(
            ValidationError, match="missing required attribute 'Section'"
        ):
            ComplianceFramework(
                framework="Test",
                name="Test",
                description="d",
                requirements=[
                    UniversalComplianceRequirement(
                        id="1.1",
                        description="d",
                        attributes={"Level": "high"},
                        checks={},
                    ),
                ],
                attributes_metadata=self._metadata(),
            )

    def test_invalid_enum_value_raises(self):
        with pytest.raises(ValidationError, match="not in"):
            ComplianceFramework(
                framework="Test",
                name="Test",
                description="d",
                requirements=[
                    UniversalComplianceRequirement(
                        id="1.1",
                        description="d",
                        attributes={"Section": "IAM", "Level": "invalid"},
                        checks={},
                    ),
                ],
                attributes_metadata=self._metadata(enum=["high", "low"]),
            )

    def test_valid_enum_value_passes(self):
        fw = ComplianceFramework(
            framework="Test",
            name="Test",
            description="d",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="d",
                    attributes={"Section": "IAM", "Level": "high"},
                    checks={},
                ),
            ],
            attributes_metadata=self._metadata(enum=["high", "low"]),
        )
        assert len(fw.requirements) == 1

    def test_wrong_type_int_raises(self):
        with pytest.raises(ValidationError, match="expected type int"):
            ComplianceFramework(
                framework="Test",
                name="Test",
                description="d",
                requirements=[
                    UniversalComplianceRequirement(
                        id="1.1",
                        description="d",
                        attributes={"Section": "IAM", "Level": "not_a_number"},
                        checks={},
                    ),
                ],
                attributes_metadata=self._metadata(type_str="int"),
            )

    def test_correct_type_int_passes(self):
        fw = ComplianceFramework(
            framework="Test",
            name="Test",
            description="d",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="d",
                    attributes={"Section": "IAM", "Level": 5},
                    checks={},
                ),
            ],
            attributes_metadata=self._metadata(type_str="int"),
        )
        assert fw.requirements[0].attributes["Level"] == 5

    def test_none_optional_value_skips_validation(self):
        """None values for non-required keys should not trigger type/enum errors."""
        fw = ComplianceFramework(
            framework="Test",
            name="Test",
            description="d",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="d",
                    attributes={"Section": "IAM", "Level": None},
                    checks={},
                ),
            ],
            attributes_metadata=self._metadata(enum=["high", "low"]),
        )
        assert len(fw.requirements) == 1

    def test_no_metadata_skips_validation(self):
        """Frameworks without attributes_metadata should not be validated."""
        fw = ComplianceFramework(
            framework="Test",
            name="Test",
            description="d",
            requirements=[
                UniversalComplianceRequirement(
                    id="1.1",
                    description="d",
                    attributes={"anything": "goes"},
                    checks={},
                ),
            ],
        )
        assert len(fw.requirements) == 1

    def test_unknown_attribute_key_raises(self):
        """Typos like 'Sectoin' must be rejected by the schema validator."""
        with pytest.raises(ValidationError, match="unknown attribute 'Sectoin'"):
            ComplianceFramework(
                framework="Test",
                name="Test",
                description="d",
                requirements=[
                    UniversalComplianceRequirement(
                        id="1.1",
                        description="d",
                        attributes={"Sectoin": "IAM", "Level": "high"},
                        checks={},
                    ),
                ],
                attributes_metadata=self._metadata(enum=["high", "low"]),
            )

    def test_multiple_unknown_keys_all_reported(self):
        """Every unknown key must appear in the validation error (deterministic order)."""
        with pytest.raises(
            ValidationError,
            match=r"unknown attribute 'Bogus1'[\s\S]*unknown attribute 'Bogus2'",
        ):
            ComplianceFramework(
                framework="Test",
                name="Test",
                description="d",
                requirements=[
                    UniversalComplianceRequirement(
                        id="1.1",
                        description="d",
                        attributes={
                            "Section": "IAM",
                            "Level": "high",
                            "Bogus1": "x",
                            "Bogus2": "y",
                        },
                        checks={},
                    ),
                ],
                attributes_metadata=self._metadata(enum=["high", "low"]),
            )

    def test_multiple_errors_reported(self):
        """All validation errors should be collected and reported together."""
        with pytest.raises(
            ValidationError, match="missing required attribute 'Section'"
        ):
            ComplianceFramework(
                framework="Test",
                name="Test",
                description="d",
                requirements=[
                    UniversalComplianceRequirement(
                        id="1.1",
                        description="d",
                        attributes={"Level": "bad"},
                        checks={},
                    ),
                    UniversalComplianceRequirement(
                        id="1.2",
                        description="d",
                        attributes={"Level": "also_bad"},
                        checks={},
                    ),
                ],
                attributes_metadata=self._metadata(enum=["high", "low"]),
            )


class TestGetBulkUniversalEntryPoints:
    """Entry-point discovery for universal (multi-provider) compliance frameworks."""

    @staticmethod
    def _write_universal_json(directory, filename, framework, display_name):
        data = {
            "framework": framework,
            "name": display_name,
            "version": "1.0",
            "description": "External multi-provider framework",
            "requirements": [
                {
                    "id": "1",
                    "name": "Requirement 1",
                    "description": "desc",
                    "checks": {"fakeexternal": ["check_a"]},
                }
            ],
        }
        with open(os.path.join(directory, filename), "w") as f:
            json.dump(data, f)

    @staticmethod
    def _entry_point(path):
        module = MagicMock()
        module.__path__ = [path]
        ep = MagicMock()
        ep.name = "fakeexternal"
        ep.group = "prowler.compliance.universal"
        ep.load.return_value = module
        return ep

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_includes_external_universal_framework(self, mock_list_modules, mock_ep):
        mock_list_modules.return_value = []
        with tempfile.TemporaryDirectory() as ep_dir:
            self._write_universal_json(
                ep_dir, "customuniversal_1.0.json", "CustomUniversal", "Custom"
            )
            mock_ep.return_value = [self._entry_point(ep_dir)]

            bulk = get_bulk_compliance_frameworks_universal("fakeexternal")

        mock_ep.assert_called_with(group="prowler.compliance.universal")
        assert "customuniversal_1.0" in bulk
        assert bulk["customuniversal_1.0"].framework == "CustomUniversal"

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_builtin_wins_over_external_on_name_collision(
        self, mock_list_modules, mock_ep
    ):
        with (
            tempfile.TemporaryDirectory() as root,
            tempfile.TemporaryDirectory() as ep_dir,
        ):
            builtin_sub = os.path.join(root, "builtinprov")
            os.makedirs(builtin_sub)
            self._write_universal_json(
                builtin_sub, "shared_1.0.json", "SharedFramework", "Built-in"
            )
            builtin_module = MagicMock()
            builtin_module.module_finder.path = root
            builtin_module.name = "prowler.compliance.builtinprov"
            mock_list_modules.return_value = [builtin_module]

            self._write_universal_json(
                ep_dir, "shared_1.0.json", "SharedFramework", "External"
            )
            mock_ep.return_value = [self._entry_point(ep_dir)]

            bulk = get_bulk_compliance_frameworks_universal("fakeexternal")

        assert "shared_1.0" in bulk
        assert bulk["shared_1.0"].name == "Built-in"

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_loads_all_frameworks_in_a_single_entry_point_path(
        self, mock_list_modules, mock_ep
    ):
        """All JSONs in one entry-point directory are added, not collapsed to one."""
        mock_list_modules.return_value = []
        with tempfile.TemporaryDirectory() as ep_dir:
            self._write_universal_json(ep_dir, "fw_a_1.0.json", "FwA", "Framework A")
            self._write_universal_json(ep_dir, "fw_b_1.0.json", "FwB", "Framework B")
            mock_ep.return_value = [self._entry_point(ep_dir)]

            bulk = get_bulk_compliance_frameworks_universal("fakeexternal")

        assert "fw_a_1.0" in bulk
        assert "fw_b_1.0" in bulk

    @patch("prowler.lib.check.compliance_models.importlib.metadata.entry_points")
    @patch("prowler.lib.check.compliance_models.list_compliance_modules")
    def test_merges_frameworks_from_multiple_packages_same_provider(
        self, mock_list_modules, mock_ep
    ):
        """Two packages under the same provider name are both discovered."""
        mock_list_modules.return_value = []
        with (
            tempfile.TemporaryDirectory() as dir_a,
            tempfile.TemporaryDirectory() as dir_b,
        ):
            self._write_universal_json(dir_a, "pkg_a_1.0.json", "PkgA", "Package A")
            self._write_universal_json(dir_b, "pkg_b_1.0.json", "PkgB", "Package B")
            mock_ep.return_value = [
                self._entry_point(dir_a),
                self._entry_point(dir_b),
            ]

            bulk = get_bulk_compliance_frameworks_universal("fakeexternal")

        assert "pkg_a_1.0" in bulk
        assert "pkg_b_1.0" in bulk
