# #region Test.DatasetReview.OrchestratorHelpers [C:3] [TYPE Module] [SEMANTICS test,dataset,orchestrator,helpers,snapshot,blockers]
# @BRIEF Tests for orchestrator_pkg/_helpers.py — pure helper functions: parse_dataset_selection, build_initial_profile,
#        build_partial_recovery_findings, extract_effective_filter_value, build_execution_snapshot, build_launch_blockers,
#        get_latest_preview, compute_preview_fingerprint.
# @RELATION BINDS_TO -> [OrchestratorHelpers]
# @TEST_EDGE: parse_dataset_selection -> Numeric, prefixed, and invalid inputs
# @TEST_EDGE: build_initial_profile -> With/without parsed context, partial recovery
# @TEST_EDGE: build_partial_recovery_findings -> Unresolved refs produce findings
# @TEST_EDGE: extract_effective_filter_value -> Dict normalised vs raw fallback
# @TEST_EDGE: build_execution_snapshot -> Full snapshot with mappings and blockers
# @TEST_EDGE: build_launch_blockers -> Blocking findings, missing preview, fingerprint mismatch
# @TEST_EDGE: get_latest_preview -> By session.last_preview_id, sorted fallback, empty
# @TEST_EDGE: compute_preview_fingerprint -> Deterministic SHA256

from __future__ import annotations
from pathlib import Path
import sys

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

from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
import pytest


# ── Fixtures ────────────────────────────────────────────────

@pytest.fixture
def mock_session():
    """Build a DatasetReviewSession with all children populated for snapshot tests."""
    from src.models.dataset_review import (
        DatasetReviewSession, ApprovalState, FilterConfidenceState, FilterRecoveryStatus,
        FilterSource, ImportedFilter, TemplateVariable, ExecutionMapping, MappingMethod,
        MappingStatus, SessionPhase, ReadinessState, SessionStatus, VariableKind,
        CompiledPreview, PreviewStatus, ValidationFinding, FindingArea, FindingSeverity,
        ResolutionState, SemanticFieldEntry, FieldProvenance,
    )

    session = MagicMock(spec=DatasetReviewSession)
    session.session_id = "sess-1"
    session.dataset_id = 42
    session.dataset_ref = "schema.dataset_name"
    session.user_id = "user-1"
    session.environment_id = "env-1"
    session.version = 1
    session.current_phase = MagicMock(value="review")
    session.readiness_state = MagicMock(value="review_ready")
    session.recommended_action = MagicMock(value="launch_dataset")
    session.status = MagicMock(value="active")

    # ImportedFilters
    imported_filter1 = MagicMock(spec=ImportedFilter)
    imported_filter1.filter_id = "filt-1"
    imported_filter1.filter_name = "region"
    imported_filter1.display_name = "Region"
    imported_filter1.raw_value = "us"
    imported_filter1.normalized_value = "US"
    imported_filter1.source = MagicMock(value="superset_url")
    imported_filter1.confidence_state = MagicMock(value="confirmed")

    imported_filter2 = MagicMock(spec=ImportedFilter)
    imported_filter2.filter_id = "filt-2"
    imported_filter2.filter_name = "date_range"
    imported_filter2.display_name = "Date Range"
    imported_filter2.raw_value = "2024-01-01"
    imported_filter2.normalized_value = {"filter_clauses": ["date >= '2024-01-01'"]}

    session.imported_filters = [imported_filter1, imported_filter2]

    # TemplateVariables
    tv1 = MagicMock(spec=TemplateVariable)
    tv1.variable_id = "tv-1"
    tv1.variable_name = "region"
    tv1.is_required = True
    tv1.default_value = None

    tv2 = MagicMock(spec=TemplateVariable)
    tv2.variable_id = "tv-2"
    tv2.variable_name = "date_range"
    tv2.is_required = False
    tv2.default_value = "2024-06-01"

    session.template_variables = [tv1, tv2]

    # ExecutionMappings
    mapping1 = MagicMock(spec=ExecutionMapping)
    mapping1.mapping_id = "map-1"
    mapping1.filter_id = "filt-1"
    mapping1.variable_id = "tv-1"
    mapping1.effective_value = "US"
    mapping1.raw_input_value = "us"
    mapping1.approval_state = ApprovalState.APPROVED
    mapping1.requires_explicit_approval = False

    mapping2 = MagicMock(spec=ExecutionMapping)
    mapping2.mapping_id = "map-2"
    mapping2.filter_id = "filt-2"
    mapping2.variable_id = "tv-2"
    mapping2.effective_value = None  # will fallback to normalized_value
    mapping2.raw_input_value = "2024-01-01"
    mapping2.approval_state = ApprovalState.PENDING
    mapping2.requires_explicit_approval = True

    session.execution_mappings = [mapping1, mapping2]

    # SemanticFields
    field1 = MagicMock(spec=SemanticFieldEntry)
    field1.field_id = "sf-1"
    field1.is_locked = True
    field1.needs_review = False
    field1.provenance = MagicMock(value="exact")

    session.semantic_fields = [field1]

    # Previews
    preview1 = MagicMock(spec=CompiledPreview)
    preview1.preview_id = "pv-1"
    preview1.created_at = datetime(2024, 6, 1, tzinfo=timezone.utc)
    preview1.preview_status = PreviewStatus.READY
    preview1.preview_fingerprint = "fingerprint-a"
    preview1.compiled_sql = "SELECT * FROM table"

    preview2 = MagicMock(spec=CompiledPreview)
    preview2.preview_id = "pv-2"
    preview2.created_at = datetime(2024, 6, 2, tzinfo=timezone.utc)
    preview2.preview_status = PreviewStatus.READY
    preview2.preview_fingerprint = "fingerprint-b"
    preview2.compiled_sql = "SELECT * FROM table_v2"

    session.previews = [preview1, preview2]
    session.last_preview_id = "pv-2"

    # Findings
    finding1 = MagicMock(spec=ValidationFinding)
    finding1.finding_id = "find-1"
    finding1.area = FindingArea.SOURCE_INTAKE
    finding1.severity = FindingSeverity.BLOCKING
    finding1.code = "MISSING_PERMISSION"
    finding1.resolution_state = ResolutionState.OPEN

    session.findings = [finding1]

    return session


@pytest.fixture
def mock_parsed_context():
    """SupersetParsedContext-like object for recovery tests."""
    ctx = MagicMock()
    ctx.partial_recovery = True
    ctx.unresolved_references = ["dataset_column_missing", "filter_value_ambiguous"]
    ctx.dataset_id = 42
    ctx.dataset_ref = "schema.dataset_name"
    ctx.dataset_payload = {"id": 42, "table_name": "dataset_name"}
    return ctx


# ── Tests for parse_dataset_selection ───────────────────────

class TestParseDatasetSelection:
    """parse_dataset_selection — source_input normalization."""

    def test_numeric_input(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import parse_dataset_selection
        ref, did = parse_dataset_selection("42")
        assert ref == "dataset:42"
        assert did == 42

    def test_dataset_prefix_with_numeric(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import parse_dataset_selection
        ref, did = parse_dataset_selection("dataset:99")
        assert ref == "dataset:99"
        assert did == 99

    def test_dataset_prefix_non_numeric(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import parse_dataset_selection
        ref, did = parse_dataset_selection("dataset:abc")
        assert ref == "dataset:abc"
        assert did is None

    def test_arbitrary_string(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import parse_dataset_selection
        ref, did = parse_dataset_selection("schema.table")
        assert ref == "schema.table"
        assert did is None

    def test_empty_input_raises(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import parse_dataset_selection
        with pytest.raises(ValueError, match="non-empty"):
            parse_dataset_selection("")
        with pytest.raises(ValueError, match="non-empty"):
            parse_dataset_selection("   ")

    def test_whitespace_stripped(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import parse_dataset_selection
        ref, did = parse_dataset_selection("  55  ")
        assert ref == "dataset:55"
        assert did == 55


# ── Tests for build_initial_profile ────────────────────────

class TestBuildInitialProfile:
    """build_initial_profile — initial DatasetProfile creation."""

    def test_with_dataset_ref(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_initial_profile
        profile = build_initial_profile("sess-1", None, "schema.dataset_name")
        assert profile.session_id == "sess-1"
        assert profile.dataset_name == "dataset_name"
        assert profile.schema_name == "schema"
        assert profile.confidence_state.value == "mostly_confirmed"
        assert profile.completeness_score == 0.25
        assert profile.manual_summary_locked is False

    def test_with_partial_recovery(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_initial_profile
        ctx = MagicMock()
        ctx.partial_recovery = True
        profile = build_initial_profile("sess-1", ctx, "schema.dataset_name")
        assert profile.confidence_state.value == "mixed"
        assert profile.has_warning_findings is True
        assert profile.has_blocking_findings is False

    def test_without_dataset_ref(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_initial_profile
        profile = build_initial_profile("sess-1", None, "")
        assert profile.dataset_name == "Unresolved dataset"
        assert profile.schema_name is None
        assert profile.confidence_state.value == "mostly_confirmed"


# ── Tests for build_partial_recovery_findings ──────────────

class TestBuildPartialRecoveryFindings:
    """build_partial_recovery_findings — mapping unresolved refs to findings."""

    def test_with_unresolved_references(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_partial_recovery_findings
        ctx = MagicMock()
        ctx.unresolved_references = ["ref_a", "ref_b"]
        findings = build_partial_recovery_findings(ctx)
        assert len(findings) == 2
        assert findings[0].area.value == "source_intake"
        assert findings[0].severity.value == "warning"
        assert findings[0].caused_by_ref == "ref_a"

    def test_without_unresolved_references(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_partial_recovery_findings
        ctx = MagicMock()
        ctx.unresolved_references = []
        findings = build_partial_recovery_findings(ctx)
        assert len(findings) == 0

    def test_missing_unresolved_refs_attribute(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_partial_recovery_findings
        ctx = MagicMock(spec=[])
        findings = build_partial_recovery_findings(ctx)
        assert len(findings) == 0


# ── Tests for extract_effective_filter_value ───────────────

class TestExtractEffectiveFilterValue:
    """extract_effective_filter_value — normalized dict vs raw fallback."""

    def test_dict_normalized_returns_raw(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import extract_effective_filter_value
        result = extract_effective_filter_value(
            {"filter_clauses": ["date >= 'x'"]}, "raw_date"
        )
        assert result == "raw_date"

    def test_dict_with_extra_form_data_returns_raw(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import extract_effective_filter_value
        result = extract_effective_filter_value(
            {"extra_form_data": {"foo": "bar"}}, "raw_val"
        )
        assert result == "raw_val"

    def test_non_dict_normalized_returns_normalized(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import extract_effective_filter_value
        result = extract_effective_filter_value("normalized_val", "raw_val")
        assert result == "normalized_val"

    def test_none_normalized_returns_raw(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import extract_effective_filter_value
        result = extract_effective_filter_value(None, "raw_val")
        assert result == "raw_val"

    def test_both_none(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import extract_effective_filter_value
        result = extract_effective_filter_value(None, None)
        assert result is None


# ── Tests for get_latest_preview ───────────────────────────

class TestGetLatestPreview:
    """get_latest_preview — resolved by last_preview_id or sorted fallback."""

    def test_by_last_preview_id(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import get_latest_preview
        session = MagicMock()
        pv1 = MagicMock()
        pv1.preview_id = "pv-1"
        pv2 = MagicMock()
        pv2.preview_id = "pv-2"
        session.previews = [pv1, pv2]
        session.last_preview_id = "pv-2"
        result = get_latest_preview(session)
        assert result.preview_id == "pv-2"

    def test_sorted_fallback_no_last_preview(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import get_latest_preview
        session = MagicMock()
        pv1 = MagicMock()
        pv1.preview_id = "pv-1"
        pv1.created_at = datetime(2024, 1, 1, tzinfo=timezone.utc)
        pv2 = MagicMock()
        pv2.preview_id = "pv-2"
        pv2.created_at = datetime(2024, 6, 1, tzinfo=timezone.utc)
        session.previews = [pv1, pv2]
        session.last_preview_id = None
        result = get_latest_preview(session)
        assert result.preview_id == "pv-2"

    def test_empty_previews_returns_none(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import get_latest_preview
        session = MagicMock()
        session.previews = []
        session.last_preview_id = None
        result = get_latest_preview(session)
        assert result is None

    def test_last_preview_id_not_in_list_falls_back_to_sorted(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import get_latest_preview
        session = MagicMock()
        pv1 = MagicMock()
        pv1.preview_id = "pv-1"
        pv1.created_at = datetime(2024, 1, 1, tzinfo=timezone.utc)
        session.previews = [pv1]
        session.last_preview_id = "pv-nonexistent"
        result = get_latest_preview(session)
        assert result.preview_id == "pv-1"


# ── Tests for compute_preview_fingerprint ──────────────────

class TestComputePreviewFingerprint:
    """compute_preview_fingerprint — deterministic SHA256."""

    def test_deterministic(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import compute_preview_fingerprint
        payload = {"dataset_id": 42, "template_params": {"region": "US"}, "effective_filters": []}
        fp1 = compute_preview_fingerprint(payload)
        fp2 = compute_preview_fingerprint(payload)
        assert fp1 == fp2
        assert len(fp1) == 64  # SHA256 hex

    def test_different_inputs(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import compute_preview_fingerprint
        fp_a = compute_preview_fingerprint({"a": 1})
        fp_b = compute_preview_fingerprint({"a": 2})
        assert fp_a != fp_b


# ── Tests for build_execution_snapshot ─────────────────────

class TestBuildExecutionSnapshot:
    """build_execution_snapshot — full snapshot from session."""

    def test_full_snapshot(self, mock_session):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_execution_snapshot
        snapshot = build_execution_snapshot(mock_session)
        assert "effective_filters" in snapshot
        assert "template_params" in snapshot
        assert "approved_mapping_ids" in snapshot
        assert "semantic_decision_refs" in snapshot
        assert "open_warning_refs" in snapshot
        assert "preview_blockers" in snapshot
        assert "preview_fingerprint" in snapshot

        # map-1 is approved, map-2 is pending + requires_explicit_approval
        assert snapshot["approved_mapping_ids"] == ["map-1"]
        assert "map-2" in snapshot["open_warning_refs"]

        # template_params should include region=US from mapping, and date_range default
        assert snapshot["template_params"]["region"] == "US"

        # effective_filters should contain both filters
        filter_names = {f["filter_name"] for f in snapshot["effective_filters"]}
        assert "region" in filter_names

    def test_missing_filter_reference_produces_blocker(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_execution_snapshot
        from src.models.dataset_review import ExecutionMapping, ImportedFilter

        session = MagicMock()
        session.imported_filters = [MagicMock(spec=ImportedFilter, filter_id="filt-1")]
        tv = MagicMock()
        tv.variable_id = "tv-1"
        tv.variable_name = "region"
        tv.default_value = None
        tv.is_required = True
        session.template_variables = [tv]
        mapping = MagicMock(spec=ExecutionMapping)
        mapping.filter_id = "filt-nonexistent"
        mapping.variable_id = "tv-1"
        mapping.effective_value = "US"
        mapping.raw_input_value = "us"
        mapping.approval_state = MagicMock(value="pending")
        mapping.requires_explicit_approval = False
        session.execution_mappings = [mapping]
        session.semantic_fields = []
        session.dataset_id = 42

        snapshot = build_execution_snapshot(session)
        assert any("missing_filter" in b for b in snapshot["preview_blockers"])

    def test_unmapped_required_variable_produces_blocker(self):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_execution_snapshot
        from src.models.dataset_review import ImportedFilter, TemplateVariable

        session = MagicMock()
        filt = MagicMock(spec=ImportedFilter)
        filt.filter_id = "filt-1"
        filt.filter_name = "region"
        filt.raw_value = "us"
        filt.normalized_value = "US"
        filt.display_name = "Region"
        session.imported_filters = [filt]
        tv = MagicMock(spec=TemplateVariable)
        tv.variable_id = "tv-1"
        tv.variable_name = "region"
        tv.is_required = True
        tv.default_value = None
        session.template_variables = [tv]
        session.execution_mappings = []  # no mappings, so variable is unmapped
        session.semantic_fields = []
        session.dataset_id = 42

        snapshot = build_execution_snapshot(session)
        assert any("unmapped" in b for b in snapshot["preview_blockers"])


# ── Tests for build_launch_blockers ────────────────────────

class TestBuildLaunchBlockers:
    """build_launch_blockers — launch gate enforcement."""

    def test_blocking_finding_creates_blocker(self, mock_session):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_launch_blockers
        blockers = build_launch_blockers(mock_session, {"preview_blockers": []}, None)
        assert any("finding:MISSING_PERMISSION:blocking" in b for b in blockers)

    def test_missing_preview_creates_blocker(self, mock_session):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_launch_blockers
        blockers = build_launch_blockers(mock_session, {"preview_blockers": []}, None)
        assert any("preview:missing" in b for b in blockers)

    def test_fingerprint_mismatch_creates_blocker(self, mock_session):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_launch_blockers, compute_preview_fingerprint
        preview = MagicMock()
        preview.preview_status = MagicMock(value="ready")
        preview.preview_status.value = "ready"
        preview.preview_fingerprint = "different-fingerprint"
        execution_fingerprint = compute_preview_fingerprint({"test": "data"})
        blockers = build_launch_blockers(
            mock_session,
            {"preview_blockers": [], "preview_fingerprint": execution_fingerprint},
            preview,
        )
        assert any("fingerprint_mismatch" in b for b in blockers)

    def test_approval_required_blocker(self, mock_session):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_launch_blockers
        from src.models.dataset_review import ApprovalState, CompiledPreview, PreviewStatus

        # Clear findings so they don't block
        mock_session.findings = []
        preview = MagicMock(spec=CompiledPreview)
        preview.preview_status = PreviewStatus.READY
        preview.preview_fingerprint = "some-fingerprint"

        blockers = build_launch_blockers(
            mock_session,
            {"preview_blockers": [], "preview_fingerprint": "some-fingerprint"},
            preview,
        )
        # map-2 requires explicit approval and is PENDING
        assert any("approval_required" in b for b in blockers)

    def test_no_blockers(self, mock_session):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_launch_blockers
        from src.models.dataset_review import ApprovalState, CompiledPreview, PreviewStatus, ResolutionState

        # Clear all findings and set all approvals
        mock_session.findings = []
        for m in mock_session.execution_mappings:
            m.approval_state = ApprovalState.APPROVED
            m.requires_explicit_approval = False

        preview = MagicMock(spec=CompiledPreview)
        preview.preview_status = PreviewStatus.READY
        preview.preview_fingerprint = "some-fingerprint"

        blockers = build_launch_blockers(
            mock_session,
            {"preview_blockers": [], "preview_fingerprint": "some-fingerprint"},
            preview,
        )
        assert len(blockers) == 0

    def test_resolved_finding_not_blocking(self, mock_session):
        from src.services.dataset_review.orchestrator_pkg._helpers import build_launch_blockers
        from src.models.dataset_review import (
            ApprovalState, CompiledPreview, PreviewStatus, ResolutionState, ValidationFinding,
        )

        # Set finding to resolved
        for f in mock_session.findings:
            f.resolution_state = ResolutionState.RESOLVED

        preview = MagicMock(spec=CompiledPreview)
        preview.preview_status = PreviewStatus.READY
        preview.preview_fingerprint = "some-fingerprint"

        blockers = build_launch_blockers(
            mock_session,
            {"preview_blockers": [], "preview_fingerprint": "some-fingerprint"},
            preview,
        )
        assert not any("finding:" in b for b in blockers)


# #endregion Test.DatasetReview.OrchestratorHelpers
