"""Validation coverage for the ConfigRequirements schema.

``Compliance_Requirement_ConfigConstraint`` is the model behind every
``ConfigRequirements`` entry in the compliance framework JSONs. These tests pin
the operator vocabulary, the value-typing rules (notably that booleans are not
coerced to integers), and that constraints survive the legacy → universal
adaptation used by the App backend and the OCSF/table outputs.
"""

import json
import pathlib

import pytest
from pydantic.v1 import ValidationError

from prowler.lib.check.compliance_models import (
    Compliance,
    Compliance_Requirement_ConfigConstraint,
    adapt_legacy_to_universal,
)

_REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
_CIS_6_0 = _REPO_ROOT / "prowler" / "compliance" / "aws" / "cis_6.0_aws.json"


def _load_cis():
    """Load the CIS 6.0 AWS framework JSON via a context manager."""
    with open(_CIS_6_0, encoding="utf-8") as f:
        return json.load(f)


class Test_Compliance_Requirement_ConfigConstraint:
    @pytest.mark.parametrize(
        "operator,value",
        [
            ("lte", 45),
            ("gte", 365),
            ("eq", False),
            ("in", [1, 2, 3]),
            ("subset", ["1.2", "1.3"]),
            ("superset", ["RSA-1024", "P-192"]),
        ],
    )
    def test_valid_operators(self, operator, value):
        c = Compliance_Requirement_ConfigConstraint(
            Check="some_check", ConfigKey="some_key", Operator=operator, Value=value
        )
        assert c.Operator == operator
        assert c.Value == value

    def test_invalid_operator_rejected(self):
        with pytest.raises(ValidationError):
            Compliance_Requirement_ConfigConstraint(
                Check="c", ConfigKey="k", Operator="between", Value=1
            )

    @pytest.mark.parametrize(
        "operator,value",
        [
            # numeric operators reject non-numeric / boolean values
            ("gte", [1, 2]),
            ("lte", ["45"]),
            ("gte", True),
            # set/list operators reject scalars
            ("subset", 5),
            ("superset", "x"),
            ("in", 1),
            # eq rejects lists
            ("eq", [1, 2]),
        ],
    )
    def test_value_type_inconsistent_with_operator_rejected(self, operator, value):
        # A mistyped Value would otherwise be silently treated as "not satisfied"
        # at runtime, forcing a spurious config-not-valid FAIL.
        with pytest.raises(ValidationError):
            Compliance_Requirement_ConfigConstraint(
                Check="c", ConfigKey="k", Operator=operator, Value=value
            )

    def test_boolean_value_not_coerced_to_int(self):
        # ``mute_non_default_regions == false`` must stay a bool, not become 0.
        c = Compliance_Requirement_ConfigConstraint(
            Check="securityhub_enabled",
            ConfigKey="mute_non_default_regions",
            Operator="eq",
            Value=False,
        )
        assert c.Value is False
        assert isinstance(c.Value, bool)

    def test_list_value_preserved_for_set_operators(self):
        c = Compliance_Requirement_ConfigConstraint(
            Check="c", ConfigKey="k", Operator="subset", Value=["1.2", "1.3"]
        )
        assert isinstance(c.Value, list)
        assert c.Value == ["1.2", "1.3"]

    def test_missing_required_fields_rejected(self):
        with pytest.raises(ValidationError):
            Compliance_Requirement_ConfigConstraint(Check="c", ConfigKey="k")

    def test_provider_defaults_to_none(self):
        # Single-provider frameworks omit Provider; it is optional.
        c = Compliance_Requirement_ConfigConstraint(
            Check="c", ConfigKey="k", Operator="eq", Value=False
        )
        assert c.Provider is None

    def test_provider_scopes_constraint(self):
        # Universal frameworks tag each constraint with the provider it applies to.
        c = Compliance_Requirement_ConfigConstraint(
            Check="securityhub_enabled",
            Provider="aws",
            ConfigKey="mute_non_default_regions",
            Operator="eq",
            Value=False,
        )
        assert c.Provider == "aws"


class Test_ConfigRequirements_On_Compliance:
    def test_requirements_without_constraints_default_to_none(self):
        compliance = Compliance(**_load_cis())
        # Requirement without configurable checks → ConfigRequirements is None.
        no_constraint = [r for r in compliance.Requirements if not r.ConfigRequirements]
        assert no_constraint
        assert no_constraint[0].ConfigRequirements is None

    def test_requirement_with_constraints_parses(self):
        compliance = Compliance(**_load_cis())
        with_constraint = [r for r in compliance.Requirements if r.ConfigRequirements]
        assert with_constraint, "cis_6.0_aws should declare ConfigRequirements"
        constraint = with_constraint[0].ConfigRequirements[0]
        assert isinstance(constraint, Compliance_Requirement_ConfigConstraint)
        assert constraint.Check
        assert constraint.Operator in {"lte", "gte", "eq", "in", "subset", "superset"}


class Test_Adapt_Legacy_To_Universal:
    def test_config_requirements_carried_to_universal(self):
        legacy = Compliance(**_load_cis())
        universal = adapt_legacy_to_universal(legacy)

        legacy_with = {r.Id for r in legacy.Requirements if r.ConfigRequirements}
        universal_with = {r.id for r in universal.requirements if r.config_requirements}
        assert legacy_with == universal_with
        assert universal_with, "expected at least one requirement with constraints"

        # The constraint payload survives as the typed constraint model with the
        # same fields (``Provider`` is carried through too, ``None`` for
        # single-provider frameworks like CIS AWS).
        sample = next(r for r in universal.requirements if r.config_requirements)
        entry = sample.config_requirements[0]
        assert isinstance(entry, Compliance_Requirement_ConfigConstraint)
        assert set(entry.dict()) == {
            "Check",
            "Provider",
            "ConfigKey",
            "Operator",
            "Value",
        }
        assert entry.Provider is None

    def test_requirements_without_constraints_are_none_in_universal(self):
        legacy = Compliance(**_load_cis())
        universal = adapt_legacy_to_universal(legacy)
        without = [r for r in universal.requirements if not r.config_requirements]
        assert without
        assert without[0].config_requirements is None
