from __future__ import annotations

import pytest

from osm_lead_source_service.domain.enums import OsmObjectType, ProfileEligibilityState
from osm_lead_source_service.domain.models import OsmIdentity, ProfileVersion
from osm_lead_source_service.errors import ContractViolation
from osm_lead_source_service.pbf import ParsedOsmObject
from osm_lead_source_service.profiles import (
    ProfileDefinition,
    ProfileEvaluator,
    ProfileLifecycleState,
    ProfileRegistry,
    ProfileRule,
    RegionRuleOverride,
    RuleEffect,
    RuleMatchMode,
)


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


def parsed(osm_id: int, *tags: tuple[str, str]) -> ParsedOsmObject:
    return ParsedOsmObject(
        identity=OsmIdentity(OsmObjectType.NODE, osm_id),
        version=1,
        tags=tags,
        longitude_e7=1,
        latitude_e7=1,
    )


def evaluator(definition: ProfileDefinition) -> ProfileEvaluator:
    return ProfileEvaluator(ProfileRegistry((definition,)))


def test_profile_versions_have_independent_golden_behavior() -> None:
    v1 = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(rule("restaurant", "restaurant"),),
        provenance=("fixture:v1",),
    )
    v2 = ProfileDefinition(
        profile=ProfileVersion("fixture", "2"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(rule("restaurant", "restaurant"), rule("cafe", "cafe")),
        provenance=("fixture:v2",),
    )
    engine = ProfileEvaluator(ProfileRegistry((v1, v2)))
    cafe = parsed(1, ("amenity", "cafe"))

    assert (
        engine.evaluate(region_id="th", parsed_object=cafe, profile=v1.profile).state
        is ProfileEligibilityState.OUT_OF_SCOPE
    )
    assert (
        engine.evaluate(region_id="th", parsed_object=cafe, profile=v2.profile).state
        is ProfileEligibilityState.ACTIVE
    )


def test_exclusion_precedes_include() -> None:
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(
            rule("include", "restaurant"),
            rule("exclude", "restaurant", effect=RuleEffect.EXCLUDE),
        ),
        provenance=("fixture",),
    )
    decision = evaluator(definition).evaluate(
        region_id="th",
        parsed_object=parsed(1, ("amenity", "restaurant")),
        profile=definition.profile,
    )
    assert decision.state is ProfileEligibilityState.EXCLUDED
    assert decision.excluded_rule_ids == ("exclude",)
    assert decision.production_insert_allowed is False


def test_conflicting_category_mappings_fail_to_pending_review() -> None:
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(
            rule("restaurant", "restaurant"),
            ProfileRule(
                rule_id="tourism",
                key="tourism",
                effect=RuleEffect.INCLUDE,
                match_mode=RuleMatchMode.EXACT,
                pattern="hotel",
                category="Hospitality",
                subcategory="Hotel",
            ),
        ),
        provenance=("fixture",),
    )
    decision = evaluator(definition).evaluate(
        region_id="th",
        parsed_object=parsed(
            1,
            ("amenity", "restaurant"),
            ("tourism", "hotel"),
        ),
        profile=definition.profile,
    )
    assert decision.state is ProfileEligibilityState.PENDING_REVIEW
    assert "conflicting_category_mappings" in decision.evidence


def test_higher_priority_mapping_resolves_conflict() -> None:
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(
            rule("restaurant", "restaurant", priority=10),
            ProfileRule(
                rule_id="tourism",
                key="tourism",
                effect=RuleEffect.INCLUDE,
                match_mode=RuleMatchMode.EXACT,
                pattern="hotel",
                category="Hospitality",
                subcategory="Hotel",
                priority=30,
            ),
        ),
        provenance=("fixture",),
    )
    decision = evaluator(definition).evaluate(
        region_id="th",
        parsed_object=parsed(
            1,
            ("amenity", "restaurant"),
            ("tourism", "hotel"),
        ),
        profile=definition.profile,
    )
    assert decision.state is ProfileEligibilityState.ACTIVE
    assert decision.subcategory == "Hotel"
    assert "selected_mapping_priority:30" in decision.evidence
    assert "suppressed_by_priority:restaurant" in decision.evidence


def test_region_override_changes_only_the_named_region() -> None:
    cafe_rule = rule("cafe", "cafe")
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(rule("restaurant", "restaurant"),),
        provenance=("fixture",),
        overrides=(RegionRuleOverride("th", additional_rules=(cafe_rule,)),),
    )
    engine = evaluator(definition)
    cafe = parsed(1, ("amenity", "cafe"))
    assert (
        engine.evaluate(region_id="th", parsed_object=cafe, profile=definition.profile).state
        is ProfileEligibilityState.ACTIVE
    )
    assert (
        engine.evaluate(region_id="au", parsed_object=cafe, profile=definition.profile).state
        is ProfileEligibilityState.OUT_OF_SCOPE
    )


def test_impact_report_is_stable_across_input_order() -> None:
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(rule("restaurant", "restaurant"),),
        provenance=("fixture",),
    )
    engine = evaluator(definition)
    objects = (
        parsed(1, ("amenity", "restaurant")),
        parsed(2, ("amenity", "school")),
    )
    first = engine.impact_report(region_id="th", objects=objects, profile=definition.profile)
    second = engine.impact_report(
        region_id="th", objects=reversed(objects), profile=definition.profile
    )
    assert first == second
    assert first.object_count == 2
    assert first.state_counts == (("active", 1), ("out_of_scope", 1))


def test_impact_report_rejects_repeated_source_identity() -> None:
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(rule("restaurant", "restaurant"),),
        provenance=("fixture",),
    )
    repeated = parsed(1, ("amenity", "restaurant"))
    with pytest.raises(ContractViolation, match="repeated source identity"):
        evaluator(definition).impact_report(
            region_id="th",
            objects=(repeated, repeated),
            profile=definition.profile,
        )


def test_profile_evaluation_prefers_canonical_lowercase_collision() -> None:
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(rule("restaurant", "restaurant"),),
        provenance=("fixture",),
    )

    decision = evaluator(definition).evaluate(
        region_id="ie",
        parsed_object=parsed(
            1,
            ("Amenity", "school"),
            ("amenity", "restaurant"),
        ),
        profile=definition.profile,
    )

    assert decision.state is ProfileEligibilityState.ACTIVE
    assert decision.matched_rule_ids == ("restaurant",)


def test_profile_evaluation_rejects_oversized_tag_values() -> None:
    definition = ProfileDefinition(
        profile=ProfileVersion("fixture", "1"),
        lifecycle_state=ProfileLifecycleState.DRAFT,
        rules=(rule("restaurant", "restaurant"),),
        provenance=("fixture",),
    )
    with pytest.raises(ContractViolation, match="tag value exceeds"):
        evaluator(definition).evaluate(
            region_id="th",
            parsed_object=parsed(1, ("amenity", "x" * 4097)),
            profile=definition.profile,
        )
