"""AWS-specific schema coverage — the biggest provider, with the richest
constraint surface (CIDRs, account IDs, port ranges, enums, thresholds)."""

import pytest

from prowler.config.scan_config_schema import SCAN_CONFIG_SCHEMA
from prowler.config.schema.aws import AWSProviderConfig
from prowler.config.schema.validator import validate_provider_config


def _validate(raw):
    return validate_provider_config("aws", raw, AWSProviderConfig)


RESOURCE_LIMIT_KEYS = [
    "max_scanned_resources_per_service",
    "max_ebs_snapshots",
    "max_backup_recovery_points",
    "max_cloudwatch_log_groups",
    "max_lambda_functions",
    "max_ecs_task_definitions",
    "max_codeartifact_packages",
]


class Test_AWS_Resource_Limits:
    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    def test_positive_values_round_trip(self, key):
        assert _validate({key: 100}) == {key: 100}

    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    def test_null_values_round_trip(self, key):
        assert _validate({key: None}) == {key: None}

    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    def test_zero_disable_sentinel_round_trips(self, key):
        assert _validate({key: 0}) == {key: 0}

    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    def test_numeric_strings_are_coerced_to_int(self, key):
        assert _validate({key: "100"}) == {key: 100}

    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    def test_disable_sentinel_minus_one_round_trips(self, key):
        assert _validate({key: -1}) == {key: -1}

    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    @pytest.mark.parametrize("value", [True, False])
    def test_booleans_are_dropped_not_coerced_to_int(self, key, value):
        assert _validate({key: value}) == {}

    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    def test_invalid_strings_are_dropped(self, key):
        assert _validate({key: "not-an-int"}) == {}

    @pytest.mark.parametrize("key", RESOURCE_LIMIT_KEYS)
    def test_keys_are_exposed_in_scan_config_schema(self, key):
        assert key in SCAN_CONFIG_SCHEMA["properties"]["aws"]["properties"]


class Test_AWS_Threat_Detection_Thresholds:
    """All threat detection thresholds are documented as fractions in 0..1.
    The biggest risk of mistyping them is silently disabling the check."""

    @pytest.mark.parametrize(
        "key",
        [
            "threat_detection_privilege_escalation_threshold",
            "threat_detection_enumeration_threshold",
            "threat_detection_llm_jacking_threshold",
        ],
    )
    def test_valid_boundary_values(self, key):
        assert _validate({key: 0.0}) == {key: 0.0}
        assert _validate({key: 1.0}) == {key: 1.0}
        assert _validate({key: 0.5}) == {key: 0.5}

    @pytest.mark.parametrize(
        "key",
        [
            "threat_detection_privilege_escalation_threshold",
            "threat_detection_enumeration_threshold",
            "threat_detection_llm_jacking_threshold",
        ],
    )
    def test_invalid_values_are_dropped(self, key):
        # 20 instead of 0.2 — would never trigger
        assert _validate({key: 20}) == {}
        # negative
        assert _validate({key: -0.1}) == {}
        # string
        assert _validate({key: "high"}) == {}


class Test_AWS_Trusted_Account_Ids:
    def test_valid_twelve_digit_ids(self):
        ids = ["123456789012", "098765432109"]
        assert _validate({"trusted_account_ids": ids}) == {"trusted_account_ids": ids}

    def test_empty_list_is_valid(self):
        assert _validate({"trusted_account_ids": []}) == {"trusted_account_ids": []}

    def test_short_id_is_dropped(self):
        assert _validate({"trusted_account_ids": ["12345"]}) == {}

    def test_non_numeric_id_is_dropped(self):
        assert _validate({"trusted_account_ids": ["1234abcd5678"]}) == {}

    def test_id_with_dashes_is_dropped(self):
        # Some users format account IDs as "1234-5678-9012"
        assert _validate({"trusted_account_ids": ["1234-5678-9012"]}) == {}


class Test_AWS_Trusted_Ips:
    def test_single_ipv4_address(self):
        assert _validate({"trusted_ips": ["1.2.3.4"]}) == {"trusted_ips": ["1.2.3.4"]}

    def test_ipv4_cidr(self):
        assert _validate({"trusted_ips": ["10.0.0.0/8"]}) == {
            "trusted_ips": ["10.0.0.0/8"]
        }

    def test_ipv6_address(self):
        assert _validate({"trusted_ips": ["2001:db8::1"]}) == {
            "trusted_ips": ["2001:db8::1"]
        }

    def test_ipv6_cidr(self):
        assert _validate({"trusted_ips": ["2001:db8::/32"]}) == {
            "trusted_ips": ["2001:db8::/32"]
        }

    def test_mixed_list(self):
        ips = ["1.2.3.4", "10.0.0.0/8", "2001:db8::1"]
        assert _validate({"trusted_ips": ips}) == {"trusted_ips": ips}

    def test_garbage_entry_is_dropped(self):
        assert _validate({"trusted_ips": ["definitely-not-an-ip"]}) == {}

    def test_cidr_with_host_bits_is_accepted(self):
        # We use strict=False so "10.0.0.5/8" is accepted. This matches the
        # behaviour of most security tools and avoids surprising users who
        # paste real-world allowlists with non-canonical CIDR notation.
        assert _validate({"trusted_ips": ["10.0.0.5/8"]}) == {
            "trusted_ips": ["10.0.0.5/8"]
        }


class Test_AWS_Ports:
    def test_valid_ports_in_range(self):
        ports = [25, 80, 443, 65535, 1]
        assert _validate({"ec2_high_risk_ports": ports}) == {
            "ec2_high_risk_ports": ports
        }

    def test_port_zero_is_dropped(self):
        # Port 0 is reserved and not a valid security signal.
        assert _validate({"ec2_high_risk_ports": [0]}) == {}

    def test_out_of_range_port_is_dropped(self):
        assert _validate({"ec2_high_risk_ports": [70000]}) == {}

    def test_negative_port_is_dropped(self):
        assert _validate({"ec2_high_risk_ports": [-1]}) == {}


class Test_AWS_Enums:
    @pytest.mark.parametrize("level", ["CRITICAL", "HIGH", "MEDIUM", "LOW"])
    def test_valid_severity_levels(self, level):
        assert _validate({"ecr_repository_vulnerability_minimum_severity": level}) == {
            "ecr_repository_vulnerability_minimum_severity": level
        }

    @pytest.mark.parametrize("level", ["critical", "Medium", "ANY", "", "X"])
    def test_invalid_severity_levels_are_dropped(self, level):
        assert _validate({"ecr_repository_vulnerability_minimum_severity": level}) == {}


class TestAWSELBv2PQCTLSAllowedPolicies:
    def test_valid_policy_list_round_trips(self):
        policies = [
            "ELBSecurityPolicy-TLS13-1-2-Res-2021-06",
            "ELBSecurityPolicy-TLS13-1-3-2021-06",
        ]

        assert _validate({"elbv2_listener_pqc_tls_allowed_policies": policies}) == {
            "elbv2_listener_pqc_tls_allowed_policies": policies
        }

    def test_key_is_exposed_in_scan_config_schema(self):
        aws_properties = SCAN_CONFIG_SCHEMA["properties"]["aws"]["properties"]

        assert "elbv2_listener_pqc_tls_allowed_policies" in aws_properties

    @pytest.mark.parametrize(
        "value",
        [
            "ELBSecurityPolicy-TLS13-1-2-Res-2021-06",
            ["ELBSecurityPolicy-TLS13-1-2-Res-2021-06", 123],
        ],
    )
    def test_invalid_policy_values_are_dropped(self, value):
        assert _validate({"elbv2_listener_pqc_tls_allowed_policies": value}) == {}


class Test_AWS_Secrets_Ignore_Files:
    def test_valid_file_patterns_round_trip(self):
        files = ["*.deps.json", "vendor/*.js"]
        assert _validate({"secrets_ignore_files": files}) == {
            "secrets_ignore_files": files
        }

    def test_empty_list_is_valid(self):
        assert _validate({"secrets_ignore_files": []}) == {"secrets_ignore_files": []}

    def test_exposed_in_scan_config_schema(self):
        aws_properties = SCAN_CONFIG_SCHEMA["properties"]["aws"]["properties"]

        assert aws_properties["secrets_ignore_files"] == {
            "anyOf": [
                {"items": {"type": "string"}, "type": "array"},
                {"type": "null"},
            ],
            "default": None,
            "title": "Secrets Ignore Files",
        }


class Test_AWS_Booleans:
    @pytest.mark.parametrize(
        "key",
        [
            "mute_non_default_regions",
            "verify_premium_support_plans",
            "check_rds_instance_replicas",
        ],
    )
    def test_true_and_false_round_trip(self, key):
        assert _validate({key: True}) == {key: True}
        assert _validate({key: False}) == {key: False}

    def test_yaml_style_boolean_coercion(self):
        # YAML can produce Python str "true"/"yes" if the user quoted it.
        # Pydantic v2 deterministically coerces "yes"/"no"/"true"/"false" to a
        # real bool in lax mode, so the value is normalized rather than passed
        # through as a string (which would be dangerous for
        # verify_premium_support_plans).
        out = _validate({"verify_premium_support_plans": "yes"})
        assert "verify_premium_support_plans" in out
        assert isinstance(out["verify_premium_support_plans"], bool)
        assert out["verify_premium_support_plans"] is True


class Test_AWS_Full_Default_Config_Round_Trips:
    """Loading the real shipped defaults through the schema must produce
    exactly the same dict. This is the regression sentinel for backwards
    compatibility."""

    def test_full_default_config_round_trip(self):
        # Subset that mirrors the shipped config.yaml semantics.
        raw = {
            "mute_non_default_regions": False,
            "disallowed_regions": ["me-south-1", "me-central-1"],
            "max_unused_access_keys_days": 45,
            "max_ec2_instance_age_in_days": 180,
            "trusted_account_ids": [],
            "trusted_ips": [],
            "ecr_repository_vulnerability_minimum_severity": "MEDIUM",
            "threat_detection_privilege_escalation_threshold": 0.2,
            "threat_detection_enumeration_threshold": 0.3,
            "threat_detection_llm_jacking_threshold": 0.4,
            "ec2_high_risk_ports": [25, 110, 8088],
        }
        assert _validate(raw) == raw
