"""Round-trip tests: generate events -> extract resources + condition keys.

Validates that the CloudTrail generator produces events the simulator can
actually resolve, covering both resource ARN extraction and condition key
evaluation across all 442 AWS services.
"""

from __future__ import annotations

import re

import pytest

from npkt.engine.condition_evaluator import _get_all_service_extractors
from npkt.engine.resource_extractor import ResourceExtractor
from npkt.generators.cloudtrail_generator import CloudTrailGenerator, GeneratorConfig
from npkt.models.cloudtrail import CloudTrailEvent
from npkt.models.report import EvaluationContext

# Relaxed ARN regex - allows alphanumeric account IDs from generated events
_ARN_RE = re.compile(r"^arn:[\w\-\*]+:[\w\-\*]+:[\w\-\*]*:[\w\-\*]*:.+$")


def _generate_events(
    count: int = 2000,
    seed: int = 42,
    services: list[str] | None = None,
) -> list[dict]:
    """Generate a batch of CloudTrail events."""
    cfg = GeneratorConfig(services=services, count=count, seed=seed)
    gen = CloudTrailGenerator(cfg)
    return gen.generate()


def _to_cloudtrail_events(raw: list[dict]) -> list[CloudTrailEvent]:
    """Convert raw dicts to CloudTrailEvent models."""
    events = []
    for d in raw:
        try:
            events.append(CloudTrailEvent.from_dict(d))
        except Exception:
            continue
    return events


class TestResourceResolutionRate:
    """Validate overall resource ARN resolution rate from generated events."""

    @pytest.fixture(scope="class")
    def generated(self) -> list[dict]:
        return _generate_events(count=2000, seed=42)

    @pytest.fixture(scope="class")
    def events(self, generated: list[dict]) -> list[CloudTrailEvent]:
        return _to_cloudtrail_events(generated)

    @pytest.fixture(scope="class")
    def extractor(self) -> ResourceExtractor:
        return ResourceExtractor()

    def test_all_events_parse(self, generated: list[dict], events: list[CloudTrailEvent]):
        """All generated events should parse into CloudTrailEvent models."""
        assert len(events) == len(generated)

    def test_resolution_rate_above_threshold(
        self, events: list[CloudTrailEvent], extractor: ResourceExtractor
    ):
        """Overall ARN resolution rate should be >= 80%."""
        has_params = [e for e in events if e.request_parameters]
        resolved = sum(1 for e in has_params if extractor.extract(e) is not None)
        rate = resolved / len(has_params) if has_params else 0
        assert rate >= 0.80, (
            f"Resolution rate {rate:.1%} ({resolved}/{len(has_params)}) "
            f"is below 80% threshold"
        )

    def test_arn_format_validity(
        self, events: list[CloudTrailEvent], extractor: ResourceExtractor
    ):
        """All resolved ARNs should match valid ARN format."""
        invalid = []
        for event in events:
            arn = extractor.extract(event)
            if arn and not _ARN_RE.match(arn):
                invalid.append((event.iam_action, arn))

        assert not invalid, (
            f"{len(invalid)} invalid ARN(s) found:\n"
            + "\n".join(f"  {action}: {arn}" for action, arn in invalid[:10])
        )


class TestPerServiceResolution:
    """Per-service resolution rate report."""

    @pytest.fixture(scope="class")
    def results(self) -> dict[str, tuple[int, int]]:
        """Build per-service resolution stats."""
        raw = _generate_events(count=3000, seed=123)
        events = _to_cloudtrail_events(raw)
        extractor = ResourceExtractor()

        stats: dict[str, tuple[int, int]] = {}
        for event in events:
            svc = event.event_source.split(".")[0]
            total, resolved = stats.get(svc, (0, 0))
            total += 1
            if extractor.extract(event) is not None:
                resolved += 1
            stats[svc] = (total, resolved)
        return stats

    def test_per_service_report(self, results: dict[str, tuple[int, int]]):
        """At least 60% of services should resolve at least one event."""
        resolved_svcs = sum(1 for _, (t, r) in results.items() if r > 0)
        total_svcs = len(results)

        assert resolved_svcs / total_svcs >= 0.60, (
            f"Only {resolved_svcs}/{total_svcs} services resolve at least one ARN"
        )


class TestMigratedServicesResolve:
    """Services migrated from Layer 2 to Layer 4 should still resolve."""

    # Services that were previously hardcoded and are now data-driven
    MIGRATED = [
        "dynamodb",
        "lambda",
        "sns",
        "kinesis",
        "secretsmanager",
        "logs",
        "ecs",
        "codebuild",
        "apigateway",
        "ecr",
        "redshift",
        "sagemaker",
        "athena",
        "guardduty",
        "access-analyzer",
        "acm",
        "cognito-idp",
        "appsync",
        "securityhub",
        "backup",
    ]

    @pytest.fixture(scope="class")
    def extractor(self) -> ResourceExtractor:
        return ResourceExtractor()

    @pytest.mark.parametrize("service", MIGRATED)
    def test_migrated_service_resolves(self, service: str, extractor: ResourceExtractor):
        """Each migrated service should resolve at least one event."""
        raw = _generate_events(count=20, seed=99, services=[service])
        events = _to_cloudtrail_events(raw)
        events_with_params = [e for e in events if e.request_parameters]

        if not events_with_params:
            pytest.skip(f"No events with params generated for {service}")

        resolved = [e for e in events_with_params if extractor.extract(e) is not None]
        assert len(resolved) > 0, (
            f"Service {service} failed to resolve any of "
            f"{len(events_with_params)} events with requestParameters"
        )


class TestQuirkyServicesParseAndResolveExistingTests:
    """Quirky services are validated by per-service test suites.

    Here we just verify generated events parse correctly and the generator
    produces events for these services.  Resolution correctness is covered
    by tests/test_services/test_<service>.py with hand-crafted events.
    """

    QUIRKY = [
        "s3",
        "ec2",
        "iam",
        "sqs",
        "kms",
        "eks",
        "glue",
        "ssm",
        "route53",
        "wafv2",
        "organizations",
        "events",
        "elasticloadbalancing",
        # "monitoring" is an eventSource alias for "cloudwatch" (not an IAM ref prefix)
        "cloudformation",
        "cloudfront",
        "cloudtrail",
        "codepipeline",
        "config",
        "elasticache",
        "rds",
        "states",
    ]

    @pytest.mark.parametrize("service", QUIRKY)
    def test_quirky_service_generates_events(self, service: str):
        """Generator should produce parseable events for each quirky service."""
        raw = _generate_events(count=10, seed=77, services=[service])
        events = _to_cloudtrail_events(raw)
        assert len(events) > 0, f"No events generated for {service}"
        assert all(e.event_source for e in events)


class TestConditionKeyExtraction:
    """Auto-generated condition key extractors should resolve values from generated events."""

    def test_auto_extractors_produce_values(self):
        """At least some auto-generated extractors should return values from generated events."""
        raw = _generate_events(count=500, seed=55)
        events = _to_cloudtrail_events(raw)
        all_extractors = _get_all_service_extractors()

        hits = 0
        total_checks = 0

        for event in events:
            if not event.request_parameters:
                continue
            svc = event.event_source.split(".")[0]
            ctx = EvaluationContext.from_event(event)

            # Check extractors for this service
            for key, extractor_fn in all_extractors.items():
                if not key.startswith(f"{svc}:"):
                    continue
                total_checks += 1
                value = extractor_fn(ctx)
                if value is not None:
                    hits += 1

        # We should get at least some hits from the enriched events
        assert hits > 0, "No condition key values extracted from generated events"
        # At least 1% of checks should hit (conservative threshold)
        hit_rate = hits / total_checks if total_checks else 0
        assert hit_rate >= 0.01, (
            f"Condition key hit rate {hit_rate:.2%} ({hits}/{total_checks}) "
            f"is below 1% threshold"
        )
