from __future__ import annotations

import pytest

from osm_lead_source_service.domain.models import ProfileVersion
from osm_lead_source_service.errors import ContractViolation
from osm_lead_source_service.profiles import (
    ProfileDefinition,
    ProfileImpactReport,
    ProfileLifecycleState,
    ProfileRegistry,
    ProfileRule,
    RegionRuleOverride,
    RuleEffect,
    RuleMatchMode,
)


def include(rule_id: str, value: str = "restaurant", *, priority: int = 0) -> ProfileRule:
    return ProfileRule(
        rule_id=rule_id,
        key="amenity",
        effect=RuleEffect.INCLUDE,
        match_mode=RuleMatchMode.EXACT,
        pattern=value,
        category="Hospitality",
        subcategory="Restaurant",
        priority=priority,
    )


def definition(*rules: ProfileRule) -> ProfileDefinition:
    return ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=rules,
        provenance=("fixture",),
    )


def test_definition_digest_is_independent_of_input_rule_order() -> None:
    first = include("a")
    second = include("b", "cafe")
    assert (
        definition(first, second).definition_digest == definition(second, first).definition_digest
    )


def test_production_insert_enablement_is_structurally_rejected() -> None:
    with pytest.raises(ContractViolation, match="production inserts"):
        ProfileDefinition(
            profile=ProfileVersion("fixture", "1"),
            lifecycle_state=ProfileLifecycleState.ACTIVE,
            rules=(include("a"),),
            provenance=("fixture",),
            approval_references=("approved",),
            production_insert_enabled=True,
        )


def test_active_nonshadow_profile_requires_approval_reference() -> None:
    with pytest.raises(ContractViolation, match="approval references"):
        ProfileDefinition(
            profile=ProfileVersion("fixture", "1"),
            lifecycle_state=ProfileLifecycleState.ACTIVE,
            rules=(include("a"),),
            provenance=("fixture",),
        )


def test_shadow_profile_can_be_active_but_never_insertable() -> None:
    profile = ProfileDefinition(
        profile=ProfileVersion("shadow", "1"),
        lifecycle_state=ProfileLifecycleState.ACTIVE,
        rules=(include("a"),),
        provenance=("fixture",),
        shadow_only=True,
    )
    assert profile.production_insert_allowed is False


def test_unsafe_or_unbounded_regex_is_rejected() -> None:
    with pytest.raises(ContractViolation, match="unsupported construct"):
        ProfileRule(
            rule_id="unsafe",
            key="amenity",
            effect=RuleEffect.INCLUDE,
            match_mode=RuleMatchMode.REGEX,
            pattern="(?=restaurant)",
            category="Test",
        )
    with pytest.raises(ContractViolation, match="bounded length"):
        ProfileRule(
            rule_id="long",
            key="amenity",
            effect=RuleEffect.INCLUDE,
            match_mode=RuleMatchMode.REGEX,
            pattern="x" * 257,
            category="Test",
        )


def test_rule_priority_is_bounded_and_include_only() -> None:
    assert include("priority", priority=1000).priority == 1000
    with pytest.raises(ContractViolation, match="priority must be"):
        include("too-high", priority=1001)
    with pytest.raises(ContractViolation, match="exclude rules cannot define priority"):
        ProfileRule(
            rule_id="exclude-priority",
            key="amenity",
            effect=RuleEffect.EXCLUDE,
            match_mode=RuleMatchMode.EXACT,
            pattern="restaurant",
            priority=1,
        )


def test_override_cannot_disable_unknown_or_shadow_base_rule() -> None:
    with pytest.raises(ContractViolation, match="unknown rule"):
        ProfileDefinition(
            profile=ProfileVersion("fixture", "1"),
            lifecycle_state=ProfileLifecycleState.DRAFT,
            rules=(include("base"),),
            provenance=("fixture",),
            overrides=(RegionRuleOverride("th", disabled_rule_ids=("missing",)),),
        )
    with pytest.raises(ContractViolation, match="shadow base rules"):
        ProfileDefinition(
            profile=ProfileVersion("fixture", "1"),
            lifecycle_state=ProfileLifecycleState.DRAFT,
            rules=(include("base"),),
            provenance=("fixture",),
            overrides=(
                RegionRuleOverride(
                    "th",
                    additional_rules=(include("base", "cafe"),),
                ),
            ),
        )


def test_impact_report_requires_consistent_provenance_and_counts() -> None:
    profile = ProfileVersion("fixture", "1")
    valid = ProfileImpactReport(
        profile=profile,
        definition_digest="a" * 64,
        object_count=2,
        state_counts=(("active", 1), ("out_of_scope", 1)),
        category_counts=(("Hospitality/Restaurant", 1),),
    )
    assert valid.state_counts == (("active", 1), ("out_of_scope", 1))

    with pytest.raises(ContractViolation, match="lowercase SHA-256"):
        ProfileImpactReport(
            profile=profile,
            definition_digest="not-a-digest",
            object_count=0,
            state_counts=(),
            category_counts=(),
        )
    with pytest.raises(ContractViolation, match="keys must be unique"):
        ProfileImpactReport(
            profile=profile,
            definition_digest="a" * 64,
            object_count=2,
            state_counts=(("active", 1), ("active", 1)),
            category_counts=(("Hospitality/Restaurant", 2),),
        )
    with pytest.raises(ContractViolation, match="total object_count"):
        ProfileImpactReport(
            profile=profile,
            definition_digest="a" * 64,
            object_count=2,
            state_counts=(("active", 1),),
            category_counts=(("Hospitality/Restaurant", 1),),
        )
    with pytest.raises(ContractViolation, match="active state count"):
        ProfileImpactReport(
            profile=profile,
            definition_digest="a" * 64,
            object_count=1,
            state_counts=(("active", 1),),
            category_counts=(),
        )


def test_registry_requires_exact_version_and_rejects_duplicates() -> None:
    item = definition(include("a"))
    registry = ProfileRegistry((item,))
    assert registry.get(ProfileVersion("fixture", "1")) is item
    with pytest.raises(ContractViolation, match="not registered"):
        registry.get(ProfileVersion("fixture", "latest"))
    with pytest.raises(ContractViolation, match="duplicate profile version"):
        ProfileRegistry((item, item))


def test_repeated_regex_groups_and_malformed_contracts_fail_closed() -> None:
    with pytest.raises(ContractViolation, match="unsafe nested quantifier"):
        ProfileRule(
            rule_id="unsafe-group",
            key="amenity",
            effect=RuleEffect.INCLUDE,
            match_mode=RuleMatchMode.REGEX,
            pattern="(restaurant|cafe)+",
            category="Test",
        )

    with pytest.raises(ContractViolation, match="rules must contain"):
        ProfileDefinition(
            profile=ProfileVersion("fixture", "2"),
            lifecycle_state=ProfileLifecycleState.DRAFT,
            rules=(object(),),  # type: ignore[arg-type]
            provenance=("fixture",),
        )

    with pytest.raises(ContractViolation, match="overrides must contain"):
        ProfileDefinition(
            profile=ProfileVersion("fixture", "3"),
            lifecycle_state=ProfileLifecycleState.DRAFT,
            rules=(include("a"),),
            provenance=("fixture",),
            overrides=(object(),),  # type: ignore[arg-type]
        )

    with pytest.raises(ContractViolation, match="invalid count entries"):
        ProfileImpactReport(
            profile=ProfileVersion("fixture", "4"),
            definition_digest="a" * 64,
            object_count=1,
            state_counts=(("active", 1, 2),),  # type: ignore[arg-type]
            category_counts=(("Hospitality/Restaurant", 1),),
        )
