"""Tests for complyform.core.assessor.control_matcher."""

from __future__ import annotations

import pytest
from pydantic import ValidationError

from complyform.core.assessor.control_matcher import ControlMatcher
from complyform.core.profiles.models import (
    Check,
    ControlCrossMapping,
    CrossMappingIndex,
    MappingFile,
    ResourceMapping,
)
from complyform.core.scanner.state_parser import ResourceRecord

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


def _resource(
    resource_type: str = "aws_s3_bucket",
    address: str = "aws_s3_bucket.main",
    attributes: dict | None = None,
) -> ResourceRecord:
    return ResourceRecord(
        provider="aws",
        resource_type=resource_type,
        name="main",
        address=address,
        attributes=attributes or {},
        module=None,
        source_state="test",
    )


def _mapping_file(
    mappings: list[ResourceMapping],
    framework_id: str = "nist-800-53",
    cloud: str = "aws",
) -> MappingFile:
    return MappingFile(
        schema_version=1,
        framework_id=framework_id,
        cloud=cloud,
        mappings=mappings,
    )


def _resource_mapping(
    control_id: str = "AC-6",
    resource_types: list[str] | None = None,
    checks: list[Check] | None = None,
    native_id: str = "",
    confidence: str = "high",
    check_id: str = "CKV_AWS_1",
) -> ResourceMapping:
    return ResourceMapping(
        check_id=check_id,
        control_id=control_id,
        resource_types=resource_types or ["aws_s3_bucket"],
        native_id=native_id,
        enforcement_tier="runtime",
        confidence=confidence,
        checks=checks or [],
    )


# ---------------------------------------------------------------------------
# 1. Single resource with checks that PASS
# ---------------------------------------------------------------------------


class TestPassFindings:
    def test_single_check_pass(self) -> None:
        resource = _resource(attributes={"versioning": True})
        check = Check(
            operator="equals",
            attribute="versioning",
            expected=True,
            failure_message="Versioning must be enabled",
        )
        mapping = _mapping_file([_resource_mapping(checks=[check])])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        f = findings[0]
        assert f.status == "PASS"
        assert f.control_id == "AC-6"
        assert f.framework == "nist-800-53"
        assert f.resource_address == "aws_s3_bucket.main"
        assert f.resource_type == "aws_s3_bucket"
        assert f.severity == "MEDIUM"
        assert f.remediation == ""
        assert f.checkov_id == "CKV_AWS_1"
        assert f.confidence == "high"
        assert f.not_assessed_reason is None

    def test_pass_finding_is_frozen(self) -> None:
        resource = _resource(attributes={"versioning": True})
        check = Check(
            operator="equals",
            attribute="versioning",
            expected=True,
        )
        mapping = _mapping_file([_resource_mapping(checks=[check])])
        findings = ControlMatcher().match([resource], mapping)

        with pytest.raises(ValidationError):
            findings[0].status = "FAIL"  # type: ignore[misc]


# ---------------------------------------------------------------------------
# 2. Single resource with checks that FAIL
# ---------------------------------------------------------------------------


class TestFailFindings:
    def test_single_check_fail(self) -> None:
        resource = _resource(attributes={"versioning": False})
        check = Check(
            operator="equals",
            attribute="versioning",
            expected=True,
            failure_message="Versioning must be enabled",
        )
        mapping = _mapping_file([_resource_mapping(checks=[check])])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        f = findings[0]
        assert f.status == "FAIL"
        assert f.severity == "HIGH"
        assert f.remediation == "Versioning must be enabled"
        assert f.finding != ""

    def test_fail_when_attribute_missing(self) -> None:
        resource = _resource(attributes={})
        check = Check(
            operator="equals",
            attribute="versioning",
            expected=True,
            failure_message="Versioning must be enabled",
        )
        mapping = _mapping_file([_resource_mapping(checks=[check])])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        assert findings[0].status == "FAIL"


# ---------------------------------------------------------------------------
# 3. Multiple checks per resource_mapping: all evaluated
# ---------------------------------------------------------------------------


class TestMultipleChecks:
    def test_all_checks_produce_findings(self) -> None:
        resource = _resource(attributes={"versioning": True, "logging": False})
        checks = [
            Check(
                operator="equals",
                attribute="versioning",
                expected=True,
                failure_message="Enable versioning",
            ),
            Check(
                operator="equals",
                attribute="logging",
                expected=True,
                failure_message="Enable logging",
            ),
        ]
        mapping = _mapping_file([_resource_mapping(checks=checks)])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 2
        statuses = {f.status for f in findings}
        assert statuses == {"PASS", "FAIL"}

    def test_multiple_mappings_same_resource_type(self) -> None:
        resource = _resource(attributes={"encryption": "AES256", "versioning": True})
        rm1 = _resource_mapping(
            control_id="AC-6",
            checks=[Check(operator="equals", attribute="versioning", expected=True)],
        )
        rm2 = _resource_mapping(
            control_id="SC-13",
            check_id="CKV_AWS_2",
            checks=[
                Check(operator="equals", attribute="encryption", expected="AES256")
            ],
        )
        mapping = _mapping_file([rm1, rm2])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 2
        control_ids = {f.control_id for f in findings}
        assert control_ids == {"AC-6", "SC-13"}
        assert all(f.status == "PASS" for f in findings)


# ---------------------------------------------------------------------------
# 4. Cross-mappings populated when index provided
# ---------------------------------------------------------------------------


class TestCrossMappings:
    def test_cross_mappings_populated(self) -> None:
        resource = _resource(attributes={"versioning": True})
        check = Check(operator="equals", attribute="versioning", expected=True)
        mapping = _mapping_file([_resource_mapping(control_id="AC-6", checks=[check])])
        index = CrossMappingIndex(
            schema_version=1,
            mappings=[
                ControlCrossMapping(
                    source_framework="nist-800-53",
                    source_control="AC-6",
                    target_framework="iso-27001",
                    target_control="A.9.2.3",
                    relationship="equivalent",
                ),
            ],
        )

        findings = ControlMatcher().match(
            [resource], mapping, cross_mapping_index=index
        )

        assert len(findings) == 1
        cm = findings[0].cross_mappings
        assert cm is not None
        assert len(cm) == 1
        assert cm[0]["framework"] == "iso-27001"
        assert cm[0]["control_id"] == "A.9.2.3"

    def test_cross_mappings_none_when_no_index(self) -> None:
        resource = _resource(attributes={"versioning": True})
        check = Check(operator="equals", attribute="versioning", expected=True)
        mapping = _mapping_file([_resource_mapping(checks=[check])])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        assert findings[0].cross_mappings is None

    def test_cross_mappings_none_when_no_match_in_index(self) -> None:
        resource = _resource(attributes={"versioning": True})
        check = Check(operator="equals", attribute="versioning", expected=True)
        mapping = _mapping_file([_resource_mapping(control_id="AC-6", checks=[check])])
        index = CrossMappingIndex(
            schema_version=1,
            mappings=[
                ControlCrossMapping(
                    source_framework="nist-800-53",
                    source_control="SC-99",
                    target_framework="iso-27001",
                    target_control="A.1.1",
                ),
            ],
        )

        findings = ControlMatcher().match(
            [resource], mapping, cross_mapping_index=index
        )

        assert len(findings) == 1
        assert findings[0].cross_mappings is None


# ---------------------------------------------------------------------------
# 5. Resource type not in any mapping → no findings
# ---------------------------------------------------------------------------


class TestNoMatchingResourceType:
    def test_unmatched_resource_type_produces_no_findings(self) -> None:
        resource = _resource(
            resource_type="aws_lambda_function", address="aws_lambda_function.fn"
        )
        check = Check(operator="equals", attribute="versioning", expected=True)
        mapping = _mapping_file(
            [
                _resource_mapping(resource_types=["aws_s3_bucket"], checks=[check]),
            ]
        )

        findings = ControlMatcher().match([resource], mapping)

        assert findings == []

    def test_empty_mappings_produces_no_findings(self) -> None:
        resource = _resource()
        mapping = _mapping_file([])

        findings = ControlMatcher().match([resource], mapping)

        assert findings == []


# ---------------------------------------------------------------------------
# 6. MANUAL_REVIEW for mappings with no checks (empty checks list)
# ---------------------------------------------------------------------------


class TestManualReview:
    def test_empty_checks_triggers_manual_review(self) -> None:
        resource = _resource()
        mapping = _mapping_file([_resource_mapping(checks=[])])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        f = findings[0]
        assert f.status == "MANUAL_REVIEW"
        assert f.severity == "MEDIUM"
        assert "manual review" in f.finding.lower()
        assert f.checkov_id == "CKV_AWS_1"
        assert f.confidence == "high"

    def test_manual_review_has_remediation(self) -> None:
        resource = _resource()
        mapping = _mapping_file([_resource_mapping(control_id="AC-6", checks=[])])

        findings = ControlMatcher().match([resource], mapping)

        assert findings[0].remediation != ""
        assert "AC-6" in findings[0].remediation


# ---------------------------------------------------------------------------
# 7. native_ids populated from mapping native_id field
# ---------------------------------------------------------------------------


class TestNativeIds:
    def test_native_ids_populated_when_present(self) -> None:
        resource = _resource(attributes={"versioning": True})
        check = Check(operator="equals", attribute="versioning", expected=True)
        mapping = _mapping_file(
            [
                _resource_mapping(
                    native_id="AWS::Config::S3BucketVersioning", checks=[check]
                ),
            ]
        )

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        assert findings[0].native_ids == {
            "native_id": "AWS::Config::S3BucketVersioning"
        }

    def test_native_ids_none_when_empty(self) -> None:
        resource = _resource(attributes={"versioning": True})
        check = Check(operator="equals", attribute="versioning", expected=True)
        mapping = _mapping_file([_resource_mapping(native_id="", checks=[check])])

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        assert findings[0].native_ids is None

    def test_native_ids_on_manual_review(self) -> None:
        resource = _resource()
        mapping = _mapping_file(
            [
                _resource_mapping(native_id="SecurityHub::S3.1", checks=[]),
            ]
        )

        findings = ControlMatcher().match([resource], mapping)

        assert len(findings) == 1
        assert findings[0].status == "MANUAL_REVIEW"
        assert findings[0].native_ids == {"native_id": "SecurityHub::S3.1"}
