# #region Test.SemanticSourceResolver [C:2] [TYPE Module] [SEMANTICS test,dataset,semantic,resolver,mapping]
# @BRIEF Tests for semantic_resolver.py — semantic candidate resolution, ranking, and conflict detection.
# @RELATION BINDS_TO -> [SemanticSourceResolver]
# @TEST_EDGE: resolve_from_file -> Returns basic resolution result
# @TEST_EDGE: resolve_from_dictionary -> Exact, fuzzy, and unresolved field matching
# @TEST_EDGE: resolve_from_dictionary_validation -> Missing source_ref or rows raises ValueError
# @TEST_EDGE: resolve_from_reference_dataset -> Returns basic result
# @TEST_EDGE: rank_candidates -> Confidence ordering
# @TEST_EDGE: detect_conflicts -> Multi-candidate detection
# @TEST_EDGE: apply_field_decision -> Field merging
# @TEST_EDGE: propagate_source_version_update -> Version propagation with locks

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

from unittest.mock import MagicMock, patch
import pytest


class TestSemanticSourceResolverResolveFromFile:
    """SemanticSourceResolver.resolve_from_file — basic file resolution."""

    def test_resolve_from_file(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        result = resolver.resolve_from_file(
            {"source_ref": "upload.csv"},
            [{"field_name": "region"}],
        )
        # Source returns the source_ref value from the payload, defaulting to "uploaded_file"
        assert result.source_ref == "upload.csv"


class TestSemanticSourceResolverResolveFromDictionary:
    """SemanticSourceResolver.resolve_from_dictionary — dictionary resolution."""

    def test_exact_match(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        source_payload = {
            "source_ref": "dict_1",
            "rows": [
                {"column_name": "region", "verbose_name": "Region", "description": "Geographic region"},
                {"column_name": "product", "verbose_name": "Product", "description": "Product name"},
            ],
        }
        fields = [{"field_name": "region"}]
        result = resolver.resolve_from_dictionary(source_payload, fields)
        assert len(result.resolved_fields) == 1
        assert result.resolved_fields[0]["field_name"] == "region"
        assert result.resolved_fields[0]["provenance"] == "dictionary_exact"
        assert result.resolved_fields[0]["needs_review"] is False

    def test_fuzzy_match(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        source_payload = {
            "source_ref": "dict_1",
            "rows": [
                {"column_name": "regions", "verbose_name": "Regions", "description": "Geographic regions"},
            ],
        }
        fields = [{"field_name": "region"}]
        result = resolver.resolve_from_dictionary(source_payload, fields)
        assert len(result.resolved_fields) == 1
        # 'region' vs 'regions' has SequenceMatcher ratio ~0.92 (> 0.72 threshold) -> fuzzy match
        assert result.resolved_fields[0]["provenance"] == "fuzzy_inferred"
        assert result.resolved_fields[0]["needs_review"] is True

    def test_unresolved_field(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        source_payload = {
            "source_ref": "dict_1",
            "rows": [
                {"column_name": "product", "verbose_name": "Product"},
            ],
        }
        fields = [{"field_name": "zzz_nonexistent"}]
        result = resolver.resolve_from_dictionary(source_payload, fields)
        assert len(result.resolved_fields) == 1
        assert result.resolved_fields[0]["status"] == "unresolved"
        assert result.resolved_fields[0]["provenance"] == "unresolved"
        assert result.unresolved_fields == ["zzz_nonexistent"]
        assert result.partial_recovery is True

    def test_missing_source_ref_raises_value_error(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        with pytest.raises(ValueError, match="source_ref"):
            resolver.resolve_from_dictionary({"rows": [{"column_name": "x"}]}, [{"field_name": "x"}])

    def test_empty_rows_raises_value_error(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        with pytest.raises(ValueError, match="rows"):
            resolver.resolve_from_dictionary({"source_ref": "dict_1", "rows": []}, [{"field_name": "x"}])

    def test_non_list_rows_raises_value_error(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        with pytest.raises(ValueError, match="rows"):
            resolver.resolve_from_dictionary({"source_ref": "dict_1", "rows": "not_a_list"}, [{"field_name": "x"}])

    def test_locked_field_preserved(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        source_payload = {
            "source_ref": "dict_1",
            "rows": [{"column_name": "region", "verbose_name": "Region"}],
        }
        fields = [{"field_name": "region", "is_locked": True}]
        result = resolver.resolve_from_dictionary(source_payload, fields)
        assert len(result.resolved_fields) == 1
        assert result.resolved_fields[0]["status"] == "preserved_manual"
        assert result.resolved_fields[0]["is_locked"] is True

    def test_skip_empty_field_name(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        source_payload = {
            "source_ref": "dict_1",
            "rows": [{"column_name": "region", "verbose_name": "Region"}],
        }
        fields = [{"field_name": ""}, {"field_name": "region"}]
        result = resolver.resolve_from_dictionary(source_payload, fields)
        assert len(result.resolved_fields) == 1
        assert result.resolved_fields[0]["field_name"] == "region"

    def test_mixed_resolved_and_unresolved(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        source_payload = {
            "source_ref": "dict_1",
            "rows": [{"column_name": "region", "verbose_name": "Region"}],
        }
        fields = [
            {"field_name": "region"},
            {"field_name": "product"},
            {"field_name": "sales_amount"},
        ]
        result = resolver.resolve_from_dictionary(source_payload, fields)
        assert len(result.resolved_fields) == 3
        region_field = next(f for f in result.resolved_fields if f["field_name"] == "region")
        assert region_field["provenance"] == "dictionary_exact"
        product_field = next(f for f in result.resolved_fields if f["field_name"] == "product")
        assert product_field["status"] == "unresolved"
        assert len(result.unresolved_fields) == 2


class TestSemanticSourceResolverResolveFromReference:
    """SemanticSourceResolver.resolve_from_reference_dataset — reference dataset."""

    def test_resolve_from_reference(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        result = resolver.resolve_from_reference_dataset(
            {"source_ref": "ref_1"},
            [{"field_name": "region"}],
        )
        # Source returns the source_ref value from the payload, defaulting to "reference_dataset"
        assert result.source_ref == "ref_1"


class TestSemanticSourceResolverRankCandidates:
    """SemanticSourceResolver.rank_candidates — confidence ordering."""

    def test_exact_before_fuzzy(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import CandidateMatchType

        resolver = SemanticSourceResolver()
        candidates = [
            {"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.95, "candidate_rank": 999},
            {"match_type": CandidateMatchType.EXACT.value, "confidence_score": 1.0, "candidate_rank": 999},
        ]
        ranked = resolver.rank_candidates(candidates)
        assert ranked[0]["match_type"] == CandidateMatchType.EXACT.value
        assert ranked[1]["match_type"] == CandidateMatchType.FUZZY.value

    def test_higher_confidence_first(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import CandidateMatchType

        resolver = SemanticSourceResolver()
        candidates = [
            {"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.8, "candidate_rank": 999},
            {"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.9, "candidate_rank": 999},
        ]
        ranked = resolver.rank_candidates(candidates)
        assert ranked[0]["confidence_score"] == 0.9
        assert ranked[1]["confidence_score"] == 0.8

    def test_ranks_assigned(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import CandidateMatchType

        resolver = SemanticSourceResolver()
        candidates = [
            {"match_type": CandidateMatchType.EXACT.value, "confidence_score": 1.0, "candidate_rank": 999},
            {"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.9, "candidate_rank": 999},
            {"match_type": CandidateMatchType.FUZZY.value, "confidence_score": 0.8, "candidate_rank": 999},
        ]
        ranked = resolver.rank_candidates(candidates)
        assert ranked[0]["candidate_rank"] == 1
        assert ranked[1]["candidate_rank"] == 2
        assert ranked[2]["candidate_rank"] == 3


class TestSemanticSourceResolverDetectConflicts:
    """SemanticSourceResolver.detect_conflicts — multi-candidate detection."""

    def test_single_candidate_no_conflict(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        assert resolver.detect_conflicts([{"rank": 1}]) is False

    def test_multiple_candidates_conflict(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        assert resolver.detect_conflicts([{"rank": 1}, {"rank": 2}]) is True


class TestSemanticSourceResolverApplyFieldDecision:
    """SemanticSourceResolver.apply_field_decision — field merging."""

    def test_merge_fields(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        field_state = {"field_name": "region", "status": "unresolved"}
        decision = {"status": "resolved", "verbose_name": "Region"}
        merged = resolver.apply_field_decision(field_state, decision)
        assert merged["field_name"] == "region"
        assert merged["status"] == "resolved"
        assert merged["verbose_name"] == "Region"


class TestSemanticSourceResolverPropagateSourceVersion:
    """SemanticSourceResolver.propagate_source_version_update — version propagation."""

    def test_propagate_success(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import SemanticSource

        resolver = SemanticSourceResolver()
        source = SemanticSource(source_id="src-1", source_version="v2")

        field_mock = MagicMock()
        field_mock.source_id = "src-1"
        field_mock.is_locked = False
        field_mock.provenance = "dictionary_exact"
        field_mock.source_version = "v1"
        field_mock.needs_review = False
        field_mock.has_conflict = False

        result = resolver.propagate_source_version_update(source, [field_mock])
        assert result["propagated"] == 1
        assert result["preserved_locked"] == 0
        assert result["untouched"] == 0
        assert field_mock.source_version == "v2"
        assert field_mock.needs_review is True

    def test_preserve_locked(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import SemanticSource

        resolver = SemanticSourceResolver()
        source = SemanticSource(source_id="src-1", source_version="v2")

        field_mock = MagicMock()
        field_mock.source_id = "src-1"
        field_mock.is_locked = True
        field_mock.provenance = "manual_override"
        field_mock.source_version = "v1"

        result = resolver.propagate_source_version_update(source, [field_mock])
        assert result["propagated"] == 0
        assert result["preserved_locked"] == 1

    def test_untouched_different_source(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import SemanticSource

        resolver = SemanticSourceResolver()
        source = SemanticSource(source_id="src-1", source_version="v2")

        field_mock = MagicMock()
        field_mock.source_id = "src-2"
        field_mock.is_locked = False
        field_mock.provenance = "dictionary_exact"

        result = resolver.propagate_source_version_update(source, [field_mock])
        assert result["propagated"] == 0
        assert result["untouched"] == 1

    def test_missing_source_metadata_raises(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import SemanticSource

        resolver = SemanticSourceResolver()
        source = SemanticSource(source_id="src-1", source_version="")
        with pytest.raises(ValueError, match="source_version"):
            resolver.propagate_source_version_update(source, [])


class TestNormalizeDictionaryRow:
    """SemanticSourceResolver._normalize_dictionary_row — row normalization."""

    def test_field_name_variants(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        r1 = resolver._normalize_dictionary_row({"column_name": "col1", "verbose_name": "Col 1"})
        assert r1["field_name"] == "col1"
        r2 = resolver._normalize_dictionary_row({"name": "named_field"})
        assert r2["field_name"] == "named_field"
        r3 = resolver._normalize_dictionary_row({"field": "field_val"})
        assert r3["field_name"] == "field_val"
        r4 = resolver._normalize_dictionary_row({"field_name": "direct"})
        assert r4["field_name"] == "direct"

    def test_verbose_name_fallback(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        r1 = resolver._normalize_dictionary_row({"field_name": "a", "label": "Label A"})
        assert r1["verbose_name"] == "Label A"
        r2 = resolver._normalize_dictionary_row({"field_name": "b"})
        assert r2.get("verbose_name") is None


class TestMatchPriority:
    """SemanticSourceResolver._match_priority — priority encoding."""

    def test_priority_ordering(self):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver
        from src.models.dataset_review import CandidateMatchType

        resolver = SemanticSourceResolver()
        assert resolver._match_priority(CandidateMatchType.EXACT.value) == 0
        assert resolver._match_priority(CandidateMatchType.FUZZY.value) == 2
        assert resolver._match_priority(CandidateMatchType.GENERATED.value) == 3
        assert resolver._match_priority(None) == 99
        assert resolver._match_priority("UNKNOWN") == 99


class TestNormalizeKey:
    """SemanticSourceResolver._normalize_key — key normalization."""

    @pytest.mark.parametrize("input_val,expected", [
        ("Hello World", "helloworld"),
        ("region_code", "region_code"),
        ("UPPER_CASE", "upper_case"),  # _normalize_key preserves underscores
        (" with spaces ", "withspaces"),
        ("special!@#chars", "specialchars"),
        ("", ""),
    ])
    def test_normalize_key(self, input_val, expected):
        from src.services.dataset_review.semantic_resolver import SemanticSourceResolver

        resolver = SemanticSourceResolver()
        assert resolver._normalize_key(input_val) == expected
# #endregion Test.SemanticSourceResolver
