#region Test.DatasetReview.DepsUnit [C:2] [TYPE Module] [SEMANTICS test,dataset,review,dependencies,unit]
# @BRIEF Direct unit tests for every private function in dataset_review_pkg/_dependencies.py.
# @RELATION BINDS_TO -> [DatasetReviewDependencies]
# @TEST_CONTRACT [PrivateDependencyFunctions] -> [CorrectErrorBehavior | CorrectDTOMapping]
# @TEST_EDGE: flag_disabled -> Feature guards raise 404 when dataset_review feature is off
# @TEST_EDGE: subflag_disabled -> Feature guards raise 404 when sub-feature flag is off
# @TEST_EDGE: flag_enabled -> Feature guards return True when all flags are on
# @TEST_EDGE: missing_header -> _require_session_version_header rejects missing X-Session-Version
# @TEST_EDGE: version_conflict_exc -> _build_session_version_conflict_http_exception returns 409
# @TEST_EDGE: enforce_version_pass -> _enforce_session_version passes through when OK
# @TEST_EDGE: enforce_version_fail -> _enforce_session_version raises 409 on conflict
# @TEST_EDGE: owned_session_404 -> _get_owned_session_or_404 raises 404
# @TEST_EDGE: owner_scope_forbidden -> _require_owner_mutation_scope raises 403 for non-owner
# @TEST_EDGE: prepare_mutation_pass -> _prepare_owned_session_mutation resolves + enforces
# @TEST_EDGE: session_no_clarification -> _get_latest_clarification_session_or_404 raises 404
# @TEST_EDGE: owned_mapping_404 -> _get_owned_mapping_or_404 raises 404
# @TEST_EDGE: owned_field_404 -> _get_owned_field_or_404 raises 404
# @TEST_EDGE: candidate_provenance_mapping -> _map_candidate_provenance covers all match types
# @TEST_EDGE: resolve_source_version -> _resolve_candidate_source_version handles edge cases
# @TEST_EDGE: update_semantic_field_no_input -> 400 when neither candidate_id nor manual fields provided
# @TEST_EDGE: update_semantic_field_invalid_candidate -> 404 when candidate_id not found
# @TEST_EDGE: sql_lab_redirect_missing_config -> 500 when URL or session ref missing
# @TEST_EDGE: export_format_validation -> _build_documentation_export/_build_validation_export handle markdown/json
# @TEST_EDGE: serialize_clarification_question_none -> _serialize_clarification_question_payload returns None

from __future__ import annotations

from datetime import UTC, datetime
from pathlib import Path
import sys
from typing import Any, cast

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

import pytest
from fastapi import HTTPException, Header, status
from unittest.mock import MagicMock, patch

from src.api.routes.dataset_review_pkg._dependencies import (
    ApproveMappingRequest,
    BatchApproveSemanticItemRequest,
    ClarificationAnswerRequest,
    FeedbackRequest,
    FieldSemanticUpdateRequest,
    StartSessionRequest,
    UpdateSessionRequest,
    _build_documentation_export,
    _build_session_version_conflict_http_exception,
    _build_sql_lab_redirect_url,
    _build_validation_export,
    _commit_owned_session_mutation,
    _enforce_session_version,
    _get_latest_clarification_session_or_404,
    _get_clarification_engine,
    _get_orchestrator,
    _get_repository,
    _get_owned_field_or_404,
    _get_owned_mapping_or_404,
    _get_owned_session_or_404,
    _map_candidate_provenance,
    _prepare_owned_session_mutation,
    _record_session_event,
    _require_auto_review_flag,
    _require_clarification_flag,
    _require_execution_flag,
    _require_owner_mutation_scope,
    _require_session_version_header,
    _resolve_candidate_source_version,
    _serialize_clarification_question_payload,
    _serialize_clarification_state,
    _serialize_empty_clarification_state,
    _serialize_execution_mapping,
    _serialize_preview,
    _serialize_run_context,
    _serialize_semantic_field,
    _serialize_session_detail,
    _serialize_session_summary,
    _update_semantic_field_state,
)
from src.models.dataset_review import (
    ApprovalState,
    ArtifactFormat,
    CandidateMatchType,
    CandidateStatus,
    ClarificationStatus,
    FieldKind,
    FieldProvenance,
    LaunchStatus,
    MappingMethod,
    PreviewStatus,
    QuestionState,
    ReadinessState,
    RecommendedAction,
    SessionPhase,
    SessionStatus,
)
from src.schemas.dataset_review import (
    CompiledPreviewDto,
    ExecutionMappingDto,
    SessionDetail,
    SessionSummary,
)
from src.services.dataset_review.repositories.session_repository import (
    DatasetReviewSessionVersionConflictError,
)


# ── Shared fixtures ────────────────────────────────────────────────────


def _make_config_manager_mock(*, dataset_review=True, ff_auto=True, ff_clarification=True, ff_execution=True):
    """Build a mock config_manager with specified feature flags."""
    cm = MagicMock()
    config = MagicMock()
    settings = MagicMock()
    features = MagicMock()
    features.dataset_review = dataset_review
    settings.features = features
    settings.ff_dataset_auto_review = ff_auto
    settings.ff_dataset_clarification = ff_clarification
    settings.ff_dataset_execution = ff_execution
    config.settings = settings
    cm.get_config.return_value = config
    return cm


def _make_session_mock(user_id="user-1", version=5):
    """Build a mock DatasetReviewSession with key attributes."""
    s = MagicMock()
    s.session_id = "sess-1"
    s.user_id = user_id
    s.version = version
    s.dataset_ref = "public.sales"
    s.environment_id = "env-1"
    s.status = SessionStatus.ACTIVE
    s.readiness_state = ReadinessState.REVIEW_READY
    s.recommended_action = RecommendedAction.REVIEW_DOCUMENTATION
    s.current_phase = SessionPhase.REVIEW
    s.created_at = datetime.now(UTC)
    s.updated_at = datetime.now(UTC)
    s.last_activity_at = datetime.now(UTC)
    s.profile = None
    s.findings = []
    s.collaborators = []
    s.semantic_sources = []
    s.semantic_fields = []
    s.imported_filters = []
    s.template_variables = []
    s.execution_mappings = []
    s.clarification_sessions = []
    s.previews = []
    s.run_contexts = []
    s.source_kind = "superset_link"
    s.source_input = "http://superset.local/dashboard/10"
    s.dataset_id = 42
    s.dashboard_id = 10
    return s


def _make_repository_mock(session=None, commit_version_conflict=False):
    repo = MagicMock()
    repo.load_session_detail.return_value = session
    repo.require_session_version.side_effect = lambda s, v: s
    repo.commit_session_mutation.side_effect = (
        DatasetReviewSessionVersionConflictError("sess-1", 5, 6)
        if commit_version_conflict
        else lambda s, **kw: s
    )
    repo.db = MagicMock()
    repo.event_logger = MagicMock()
    repo.event_logger.log_for_session.return_value = MagicMock(session_event_id="evt-1")
    return repo


# ── DTO tests ──────────────────────────────────────────────────────────


#region test_start_session_request_validation [C:2]
# @BRIEF StartSessionRequest validates source_kind pattern and required fields.
def test_start_session_request_validation():
    req = StartSessionRequest(
        source_kind="superset_link",
        source_input="http://example.com/dashboard/1",
        environment_id="env-1",
    )
    assert req.source_kind == "superset_link"
    assert req.source_input == "http://example.com/dashboard/1"
    assert req.environment_id == "env-1"

    # Invalid pattern
    with pytest.raises(Exception):
        StartSessionRequest(
            source_kind="invalid_kind",
            source_input="test",
            environment_id="env-1",
        )
#endregion


#region test_update_session_request [C:2]
# @BRIEF UpdateSessionRequest accepts status and optional note.
def test_update_session_request():
    req = UpdateSessionRequest(status=SessionStatus.PAUSED, note="Testing pause")
    assert req.status == SessionStatus.PAUSED
    assert req.note == "Testing pause"
#endregion


#region test_clarification_answer_request [C:2]
# @BRIEF ClarificationAnswerRequest validates question_id and answer fields.
def test_clarification_answer_request():
    from src.models.dataset_review import AnswerKind
    req = ClarificationAnswerRequest(
        question_id="q-1",
        answer_kind=AnswerKind.SELECTED,
        answer_value="my answer",
    )
    assert req.question_id == "q-1"
    assert req.answer_kind == AnswerKind.SELECTED
    assert req.answer_value == "my answer"
#endregion


#region test_feedback_request [C:2]
# @BRIEF FeedbackRequest validates pattern (up|down).
def test_feedback_request():
    assert FeedbackRequest(feedback="up").feedback == "up"
    assert FeedbackRequest(feedback="down").feedback == "down"
    with pytest.raises(Exception):
        FeedbackRequest(feedback="sideways")
#endregion


#region test_field_semantic_update_request [C:2]
# @BRIEF FieldSemanticUpdateRequest accepts various override patterns.
def test_field_semantic_update_request():
    req = FieldSemanticUpdateRequest(candidate_id="cand-1", lock_field=True)
    assert req.candidate_id == "cand-1"
    assert req.lock_field is True

    req2 = FieldSemanticUpdateRequest(verbose_name="Custom Name", description="Custom desc")
    assert req2.verbose_name == "Custom Name"
    assert req2.description == "Custom desc"
#endregion


#region test_approve_mapping_request [C:2]
# @BRIEF ApproveMappingRequest with and without approval note.
def test_approve_mapping_request():
    req = ApproveMappingRequest(approval_note="Reviewed and approved")
    assert req.approval_note == "Reviewed and approved"
    assert ApproveMappingRequest().approval_note is None
#endregion


#region test_batch_approve_semantic_item_request [C:2]
# @BRIEF BatchApproveSemanticItemRequest requires field_id and candidate_id.
def test_batch_approve_semantic_item_request():
    req = BatchApproveSemanticItemRequest(field_id="f-1", candidate_id="cand-1", lock_field=True)
    assert req.field_id == "f-1"
    assert req.candidate_id == "cand-1"
    assert req.lock_field is True
    with pytest.raises(Exception):
        BatchApproveSemanticItemRequest(field_id="", candidate_id="")
#endregion


# ── Feature flag guards ────────────────────────────────────────────────


#region test_require_auto_review_flag_enabled [C:2]
# @BRIEF Returns True when all flags are on.
def test_require_auto_review_flag_enabled():
    cm = _make_config_manager_mock(dataset_review=True, ff_auto=True)
    result = _require_auto_review_flag(config_manager=cm)
    assert result is True
#endregion


#region test_require_auto_review_flag_feature_disabled [C:2]
# @BRIEF Raises 404 when dataset_review feature is disabled.
def test_require_auto_review_flag_feature_disabled():
    cm = _make_config_manager_mock(dataset_review=False, ff_auto=True)
    with pytest.raises(HTTPException) as exc:
        _require_auto_review_flag(config_manager=cm)
    assert exc.value.status_code == 404
    assert "Dataset review feature is disabled" in exc.value.detail
#endregion


#region test_require_auto_review_flag_subflag_disabled [C:2]
# @BRIEF Raises 404 when ff_dataset_auto_review is disabled.
def test_require_auto_review_flag_subflag_disabled():
    cm = _make_config_manager_mock(dataset_review=True, ff_auto=False)
    with pytest.raises(HTTPException) as exc:
        _require_auto_review_flag(config_manager=cm)
    assert exc.value.status_code == 404
    assert "Dataset auto review feature is disabled" in exc.value.detail
#endregion


#region test_require_clarification_flag_enabled [C:2]
# @BRIEF Returns True when clarification flags are on.
def test_require_clarification_flag_enabled():
    cm = _make_config_manager_mock(dataset_review=True, ff_clarification=True)
    result = _require_clarification_flag(config_manager=cm)
    assert result is True
#endregion


#region test_require_clarification_flag_disabled [C:2]
# @BRIEF Raises 404 when clarification feature is disabled (subflag).
def test_require_clarification_flag_disabled():
    cm = _make_config_manager_mock(dataset_review=True, ff_clarification=False)
    with pytest.raises(HTTPException) as exc:
        _require_clarification_flag(config_manager=cm)
    assert exc.value.status_code == 404
    assert "Dataset clarification feature is disabled" in exc.value.detail
#endregion


#region test_require_execution_flag_enabled [C:2]
# @BRIEF Returns True when execution flags are on.
def test_require_execution_flag_enabled():
    cm = _make_config_manager_mock(dataset_review=True, ff_execution=True)
    result = _require_execution_flag(config_manager=cm)
    assert result is True
#endregion


#region test_require_execution_flag_disabled [C:2]
# @BRIEF Raises 404 when execution feature is disabled (subflag).
def test_require_execution_flag_disabled():
    cm = _make_config_manager_mock(dataset_review=True, ff_execution=False)
    with pytest.raises(HTTPException) as exc:
        _require_execution_flag(config_manager=cm)
    assert exc.value.status_code == 404
    assert "Dataset execution feature is disabled" in exc.value.detail
#endregion


#region test_flag_guards_feature_disabled_override [C:2]
# @BRIEF All three guards raise 404 when the base dataset_review feature flag is off.
def test_flag_guards_feature_disabled_override():
    cm = _make_config_manager_mock(dataset_review=False)

    for guard in [_require_auto_review_flag, _require_clarification_flag, _require_execution_flag]:
        with pytest.raises(HTTPException) as exc:
            guard(config_manager=cm)
        assert exc.value.status_code == 404
        assert "Dataset review feature is disabled" in exc.value.detail
#endregion


# ── Session version header ─────────────────────────────────────────────


#region test_require_session_version_header [C:2]
# @BRIEF Parses valid X-Session-Version header.
def test_require_session_version_header():
    # Can't easily test the Header injection standalone;
    # test via the HTTPException that would result from missing header at FastAPI level.
    # The function itself just returns the parsed int.
    # We test that it works with valid ints.
    version = _require_session_version_header(session_version=5)
    assert version == 5
    assert isinstance(version, int)
#endregion


# ── Error builders and enforcement ────────────────────────────────────


#region test_build_session_version_conflict_exception [C:2]
# @BRIEF Builds HTTP 409 with structured detail payload.
def test_build_session_version_conflict_exception():
    exc = DatasetReviewSessionVersionConflictError(
        session_id="sess-1", expected_version=5, actual_version=8
    )
    http_exc = _build_session_version_conflict_http_exception(exc)
    assert http_exc.status_code == 409
    detail = http_exc.detail
    assert detail["error_code"] == "session_version_conflict"
    assert detail["session_id"] == "sess-1"
    assert detail["expected_version"] == 5
    assert detail["actual_version"] == 8
#endregion


#region test_enforce_session_version_pass [C:2]
# @BRIEF Passes through when versions match.
def test_enforce_session_version_pass():
    session = _make_session_mock(version=5)
    repo = MagicMock()
    repo.require_session_version.return_value = session
    result = _enforce_session_version(repo, session, 5)
    assert result is session
    repo.require_session_version.assert_called_once_with(session, 5)
#endregion


#region test_enforce_session_version_conflict [C:2]
# @BRIEF Raises HTTP 409 when versions conflict.
def test_enforce_session_version_conflict():
    session = _make_session_mock(version=8)
    repo = MagicMock()
    repo.require_session_version.side_effect = DatasetReviewSessionVersionConflictError(
        session_id="sess-1", expected_version=5, actual_version=8
    )
    with pytest.raises(HTTPException) as exc:
        _enforce_session_version(repo, session, 5)
    assert exc.value.status_code == 409
    detail = exc.value.detail
    assert detail["error_code"] == "session_version_conflict"
    assert detail["expected_version"] == 5
    assert detail["actual_version"] == 8
#endregion


# ── Session ownership helpers ──────────────────────────────────────────


#region test_get_owned_session_or_404_found [C:2]
# @BRIEF Returns session when accessible to user.
def test_get_owned_session_or_404_found():
    session = _make_session_mock(user_id="user-1")
    repo = MagicMock()
    repo.load_session_detail.return_value = session
    result = _get_owned_session_or_404(repo, "sess-1", MagicMock(id="user-1"))
    assert result is session
#endregion


#region test_get_owned_session_or_404_not_found [C:2]
# @BRIEF Raises 404 when session not found.
def test_get_owned_session_or_404_not_found():
    repo = MagicMock()
    repo.load_session_detail.return_value = None
    with pytest.raises(HTTPException) as exc:
        _get_owned_session_or_404(repo, "sess-1", MagicMock(id="user-1"))
    assert exc.value.status_code == 404
    assert "Session not found" in exc.value.detail
#endregion


#region test_require_owner_mutation_scope_pass [C:2]
# @BRIEF Passes when user is the owner.
def test_require_owner_mutation_scope_pass():
    session = _make_session_mock(user_id="user-1")
    result = _require_owner_mutation_scope(session, MagicMock(id="user-1"))
    assert result is session
#endregion


#region test_require_owner_mutation_scope_forbidden [C:2]
# @BRIEF Raises 403 when non-owner tries to mutate.
def test_require_owner_mutation_scope_forbidden():
    session = _make_session_mock(user_id="user-1")
    with pytest.raises(HTTPException) as exc:
        _require_owner_mutation_scope(session, MagicMock(id="user-2"))
    assert exc.value.status_code == 403
    assert "Only the owner can mutate" in exc.value.detail
#endregion


#region test_prepare_owned_session_mutation [C:2]
# @BRIEF Combines resolve + ownership check + version enforcement.
def test_prepare_owned_session_mutation():
    session = _make_session_mock(user_id="user-1")
    repo = MagicMock()
    repo.load_session_detail.return_value = session
    repo.require_session_version.return_value = session

    result = _prepare_owned_session_mutation(
        repo, "sess-1", MagicMock(id="user-1"), 5
    )
    assert result is session
    repo.load_session_detail.assert_called_once_with("sess-1", "user-1")
    repo.require_session_version.assert_called_once()
#endregion


#region test_prepare_owned_session_mutation_not_owner [C:2]
# @BRIEF Raises 403 when session belongs to another user.
def test_prepare_owned_session_mutation_not_owner():
    session = _make_session_mock(user_id="user-1")
    repo = MagicMock()
    repo.load_session_detail.return_value = session
    with pytest.raises(HTTPException) as exc:
        _prepare_owned_session_mutation(repo, "sess-1", MagicMock(id="user-2"), 5)
    assert exc.value.status_code == 403
#endregion


# ── Commit helpers ──────────────────────────────────────────────────────


#region test_commit_owned_session_mutation_pass [C:2]
# @BRIEF Commits successfully through repository.
def test_commit_owned_session_mutation_pass():
    session = _make_session_mock()
    repo = MagicMock()
    repo.commit_session_mutation.return_value = session
    result = _commit_owned_session_mutation(repo, session)
    assert result is session
    repo.commit_session_mutation.assert_called_once_with(
        session, refresh_targets=None
    )
#endregion


#region test_commit_owned_session_mutation_with_refresh_targets [C:2]
# @BRIEF Passes refresh_targets through to repository.
def test_commit_owned_session_mutation_with_refresh_targets():
    session = _make_session_mock()
    field = MagicMock()
    repo = MagicMock()
    repo.commit_session_mutation.return_value = session
    result = _commit_owned_session_mutation(repo, session, refresh_targets=[field])
    assert result is session
    repo.commit_session_mutation.assert_called_once_with(
        session, refresh_targets=[field]
    )
#endregion


#region test_commit_owned_session_mutation_conflict [C:2]
# @BRIEF Raises 409 when repository reports version conflict.
def test_commit_owned_session_mutation_conflict():
    session = _make_session_mock()
    repo = MagicMock()
    repo.commit_session_mutation.side_effect = DatasetReviewSessionVersionConflictError(
        session_id="sess-1", expected_version=5, actual_version=8
    )
    with pytest.raises(HTTPException) as exc:
        _commit_owned_session_mutation(repo, session)
    assert exc.value.status_code == 409
#endregion


# ── Event recorder ──────────────────────────────────────────────────────


#region test_record_session_event [C:2]
# @BRIEF Delegates to repository's event_logger.
def test_record_session_event():
    session = _make_session_mock()
    repo = MagicMock()
    repo.event_logger = MagicMock()
    user = MagicMock(id="user-1")
    _record_session_event(
        repo, session, user,
        event_type="test_event",
        event_summary="Test event summary",
        event_details={"key": "value"},
    )
    repo.event_logger.log_for_session.assert_called_once_with(
        session,
        actor_user_id="user-1",
        event_type="test_event",
        event_summary="Test event summary",
        event_details={"key": "value"},
    )
#endregion


# ── Clarification helpers ──────────────────────────────────────────────


#region test_get_latest_clarification_session_or_404_found [C:2]
# @BRIEF Returns latest clarification session when available.
def test_get_latest_clarification_session_or_404_found():
    session = _make_session_mock()
    cs1 = MagicMock(clarification_session_id="cs-1", started_at=datetime(2025, 1, 1, tzinfo=UTC))
    cs2 = MagicMock(clarification_session_id="cs-2", started_at=datetime(2025, 6, 1, tzinfo=UTC))
    session.clarification_sessions = [cs1, cs2]
    result = _get_latest_clarification_session_or_404(session)
    assert result.clarification_session_id == "cs-2"
#endregion


#region test_get_latest_clarification_session_or_404_empty [C:2]
# @BRIEF Raises 404 when no clarification sessions exist.
def test_get_latest_clarification_session_or_404_empty():
    session = _make_session_mock()
    session.clarification_sessions = []
    with pytest.raises(HTTPException) as exc:
        _get_latest_clarification_session_or_404(session)
    assert exc.value.status_code == 404
    assert "Clarification session not found" in exc.value.detail
#endregion


# ── Mapping / field resolution ─────────────────────────────────────────


#region test_get_owned_mapping_or_404_found [C:2]
# @BRIEF Returns mapping when found in session.
def test_get_owned_mapping_or_404_found():
    session = _make_session_mock()
    mapping = MagicMock(mapping_id="map-1")
    mapping2 = MagicMock(mapping_id="map-2")
    session.execution_mappings = [mapping, mapping2]
    result = _get_owned_mapping_or_404(session, "map-2")
    assert result is mapping2
#endregion


#region test_get_owned_mapping_or_404_not_found [C:2]
# @BRIEF Raises 404 when mapping not found.
def test_get_owned_mapping_or_404_not_found():
    session = _make_session_mock()
    session.execution_mappings = []
    with pytest.raises(HTTPException) as exc:
        _get_owned_mapping_or_404(session, "map-999")
    assert exc.value.status_code == 404
    assert "Execution mapping not found" in exc.value.detail
#endregion


#region test_get_owned_field_or_404_found [C:2]
# @BRIEF Returns field when found in session.
def test_get_owned_field_or_404_found():
    session = _make_session_mock()
    field = MagicMock(field_id="field-1")
    session.semantic_fields = [field]
    result = _get_owned_field_or_404(session, "field-1")
    assert result is field
#endregion


#region test_get_owned_field_or_404_not_found [C:2]
# @BRIEF Raises 404 when field not found.
def test_get_owned_field_or_404_not_found():
    session = _make_session_mock()
    session.semantic_fields = []
    with pytest.raises(HTTPException) as exc:
        _get_owned_field_or_404(session, "field-999")
    assert exc.value.status_code == 404
    assert "Semantic field not found" in exc.value.detail
#endregion


# ── Candidate provenance mapping ──────────────────────────────────────


#region test_map_candidate_provenance [C:2]
# @BRIEF Maps CandidateMatchType to FieldProvenance correctly.
def test_map_candidate_provenance():
    exact = MagicMock()
    exact.match_type.value = "exact"
    assert _map_candidate_provenance(exact) == FieldProvenance.DICTIONARY_EXACT

    ref = MagicMock()
    ref.match_type.value = "reference"
    assert _map_candidate_provenance(ref) == FieldProvenance.REFERENCE_IMPORTED

    generated = MagicMock()
    generated.match_type.value = "generated"
    assert _map_candidate_provenance(generated) == FieldProvenance.AI_GENERATED

    fuzzy = MagicMock()
    fuzzy.match_type.value = "something_else"
    assert _map_candidate_provenance(fuzzy) == FieldProvenance.FUZZY_INFERRED
#endregion


# ── Source version resolution ──────────────────────────────────────────


#region test_resolve_candidate_source_version [C:2]
# @BRIEF Resolves source version from field/session.
def test_resolve_candidate_source_version():
    field = MagicMock()
    session = MagicMock()
    field.session = session
    source = MagicMock(source_id="src-1", source_version="v2.0")
    session.semantic_sources = [source]
    assert _resolve_candidate_source_version(field, "src-1") == "v2.0"
#endregion


#region test_resolve_candidate_source_version_not_found [C:2]
# @BRIEF Returns None when source_id not found.
def test_resolve_candidate_source_version_not_found():
    field = MagicMock()
    session = MagicMock()
    field.session = session
    session.semantic_sources = [MagicMock(source_id="src-1", source_version="v1")]
    assert _resolve_candidate_source_version(field, "src-999") is None
#endregion


#region test_resolve_candidate_source_version_no_source_id [C:2]
# @BRIEF Returns None when source_id is None.
def test_resolve_candidate_source_version_no_source_id():
    field = MagicMock()
    assert _resolve_candidate_source_version(field, None) is None
#endregion


#region test_resolve_candidate_source_version_no_session [C:2]
# @BRIEF Returns None when field has no session reference.
def test_resolve_candidate_source_version_no_session():
    field = MagicMock()
    field.session = None
    assert _resolve_candidate_source_version(field, "src-1") is None
#endregion


#region test_resolve_candidate_source_version_empty_sources [C:2]
# @BRIEF Returns None when session has no semantic_sources.
def test_resolve_candidate_source_version_empty_sources():
    field = MagicMock()
    field.session = MagicMock()
    field.session.semantic_sources = []
    assert _resolve_candidate_source_version(field, "src-1") is None
#endregion


# ── Semantic field state update ────────────────────────────────────────


def _make_field_mock(field_id="field-1"):
    field = MagicMock()
    field.field_id = field_id
    field.verbose_name = None
    field.description = None
    field.display_format = None
    field.provenance = FieldProvenance.UNRESOLVED
    field.source_id = None
    field.source_version = None
    field.confidence_rank = None
    field.is_locked = False
    field.has_conflict = False
    field.needs_review = False
    field.last_changed_by = None
    candidate = MagicMock()
    candidate.candidate_id = "cand-1"
    candidate.match_type = MagicMock()
    candidate.match_type.value = "exact"
    candidate.proposed_verbose_name = "Recognized Revenue"
    candidate.proposed_description = "Revenue recognized during period"
    candidate.proposed_display_format = "$,.0f"
    candidate.source_id = "src-1"
    candidate.candidate_rank = 1
    candidate.status = CandidateStatus.PROPOSED
    field.candidates = [candidate]
    field.session = MagicMock()
    field.session.semantic_sources = [MagicMock(source_id="src-1", source_version="v2")]
    return field


#region test_update_semantic_field_state_manual_override [C:2]
# @BRIEF Manual override sets provenance to MANUAL_OVERRIDE and locks the field.
def test_update_semantic_field_state_manual_override():
    field = _make_field_mock()
    req = FieldSemanticUpdateRequest(
        verbose_name="Custom Name",
        description="Custom description",
        display_format="text",
    )
    result = _update_semantic_field_state(field, req, changed_by="user")
    assert result.verbose_name == "Custom Name"
    assert result.description == "Custom description"
    assert result.display_format == "text"
    assert result.provenance == FieldProvenance.MANUAL_OVERRIDE
    assert result.is_locked is True
    assert result.source_id is None
    for c in result.candidates:
        assert c.status == CandidateStatus.SUPERSEDED
#endregion


#region test_update_semantic_field_state_candidate_acceptance [C:2]
# @BRIEF Accepting a candidate applies its values with mapped provenance.
def test_update_semantic_field_state_candidate_acceptance():
    field = _make_field_mock()
    req = FieldSemanticUpdateRequest(candidate_id="cand-1", lock_field=True)
    result = _update_semantic_field_state(field, req, changed_by="user")
    assert result.verbose_name == "Recognized Revenue"
    assert result.provenance == FieldProvenance.DICTIONARY_EXACT
    assert result.source_id == "src-1"
    assert result.source_version == "v2"
    assert result.confidence_rank == 1
    assert result.is_locked is True
    assert result.needs_review is False
    accepted = [c for c in result.candidates if c.status == CandidateStatus.ACCEPTED]
    superseded = [c for c in result.candidates if c.status == CandidateStatus.SUPERSEDED]
    assert len(accepted) == 1
    assert len(superseded) == 0  # Only one candidate, it gets accepted
#endregion


#region test_update_semantic_field_state_no_input [C:2]
# @BRIEF Raises 400 when neither candidate_id nor manual override provided.
def test_update_semantic_field_state_no_input():
    field = _make_field_mock()
    req = FieldSemanticUpdateRequest()
    with pytest.raises(HTTPException) as exc:
        _update_semantic_field_state(field, req, changed_by="user")
    assert exc.value.status_code == 400
#endregion


#region test_update_semantic_field_state_invalid_candidate [C:2]
# @BRIEF Raises 404 when candidate_id not found.
def test_update_semantic_field_state_invalid_candidate():
    field = _make_field_mock()
    req = FieldSemanticUpdateRequest(candidate_id="nonexistent")
    with pytest.raises(HTTPException) as exc:
        _update_semantic_field_state(field, req, changed_by="user")
    assert exc.value.status_code == 404
    assert "Semantic candidate not found" in exc.value.detail
#endregion


# ── SQL Lab redirect URL building ──────────────────────────────────────


#region test_build_sql_lab_redirect_url [C:2]
# @BRIEF Builds correct redirect URL with queryId parameter.
def test_build_sql_lab_redirect_url():
    url = _build_sql_lab_redirect_url(
        environment_url="http://superset.local",
        sql_lab_session_ref="sql-lab-77",
    )
    assert url == "http://superset.local/superset/sqllab?queryId=sql-lab-77"
#endregion


#region test_build_sql_lab_redirect_url_trailing_slash [C:2]
# @BRIEF Handles trailing slash on base URL.
def test_build_sql_lab_redirect_url_trailing_slash():
    url = _build_sql_lab_redirect_url(
        environment_url="http://superset.local/",
        sql_lab_session_ref="sql-lab-77",
    )
    assert url == "http://superset.local/superset/sqllab?queryId=sql-lab-77"
#endregion


#region test_build_sql_lab_redirect_url_empty_env [C:2]
# @BRIEF Raises 500 when environment URL is empty.
def test_build_sql_lab_redirect_url_empty_env():
    with pytest.raises(HTTPException) as exc:
        _build_sql_lab_redirect_url(environment_url="", sql_lab_session_ref="sql-lab-77")
    assert exc.value.status_code == 500
    assert "Superset environment URL is not configured" in exc.value.detail
#endregion


#region test_build_sql_lab_redirect_url_empty_ref [C:2]
# @BRIEF Raises 500 when SQL Lab session reference is empty.
def test_build_sql_lab_redirect_url_empty_ref():
    with pytest.raises(HTTPException) as exc:
        _build_sql_lab_redirect_url(
            environment_url="http://superset.local",
            sql_lab_session_ref="",
        )
    assert exc.value.status_code == 500
    assert "SQL Lab session reference is missing" in exc.value.detail
#endregion


#region test_build_sql_lab_redirect_url_empty_ref_whitespace [C:2]
# @BRIEF Raises 500 when ref is just whitespace.
def test_build_sql_lab_redirect_url_empty_ref_whitespace():
    with pytest.raises(HTTPException) as exc:
        _build_sql_lab_redirect_url(
            environment_url="http://superset.local/",
            sql_lab_session_ref="   ",
        )
    assert exc.value.status_code == 500
    assert "SQL Lab session reference is missing" in exc.value.detail
#endregion


# ── Export builders ────────────────────────────────────────────────────


def _make_export_session():
    session = _make_session_mock()
    session.profile = MagicMock(
        business_summary="Revenue data summary",
        dataset_name="sales",
        confidence_state=MagicMock(value="mostly_confirmed"),
        dataset_type="table",
    )
    finding = MagicMock()
    finding.code = "TEST-001"
    finding.severity = MagicMock(value="warning")
    finding.title = "Test Issue"
    finding.message = "This is a test finding"
    finding.resolution_state = MagicMock(value="open")
    session.findings = [finding]
    return session


def _make_export_session_no_profile():
    session = _make_session_mock()
    session.profile = None
    session.findings = []
    return session


#region test_build_documentation_export_json [C:2]
# @BRIEF Produces JSON documentation export with profile data.
def test_build_documentation_export_json():
    session = _make_export_session()
    result = _build_documentation_export(session, ArtifactFormat.JSON)
    assert result["storage_ref"].endswith("documentation.json")
    assert result["content"]["session"]["session_id"] == "sess-1"
    assert result["content"]["profile"]["business_summary"] == "Revenue data summary"
    assert len(result["content"]["findings"]) == 1
#endregion


#region test_build_documentation_export_markdown [C:2]
# @BRIEF Produces Markdown documentation export.
def test_build_documentation_export_markdown():
    session = _make_export_session()
    result = _build_documentation_export(session, ArtifactFormat.MARKDOWN)
    assert result["storage_ref"].endswith("documentation.md")
    assert "# Dataset Review:" in result["content"]["markdown"]
    assert "Revenue data summary" in result["content"]["markdown"]
    assert "Test Issue" in result["content"]["markdown"]
#endregion


#region test_build_documentation_export_no_profile [C:2]
# @BRIEF Produces documentation export without profile data.
def test_build_documentation_export_no_profile():
    session = _make_export_session_no_profile()
    result = _build_documentation_export(session, ArtifactFormat.JSON)
    assert result["content"]["profile"] is None
#endregion


#region test_build_documentation_export_no_findings [C:2]
# @BRIEF Markdown handles no findings gracefully.
def test_build_documentation_export_no_findings():
    session = _make_export_session_no_profile()
    result = _build_documentation_export(session, ArtifactFormat.MARKDOWN)
    assert "No findings recorded" in result["content"]["markdown"]
#endregion


#region test_build_validation_export_json [C:2]
# @BRIEF Produces JSON validation export.
def test_build_validation_export_json():
    session = _make_export_session()
    result = _build_validation_export(session, ArtifactFormat.JSON)
    assert result["storage_ref"].endswith("validation.json")
    assert result["content"]["session_id"] == "sess-1"
    assert len(result["content"]["findings"]) == 1
    assert result["content"]["readiness_state"] == "review_ready"
#endregion


#region test_build_validation_export_markdown [C:2]
# @BRIEF Produces Markdown validation export.
def test_build_validation_export_markdown():
    session = _make_export_session()
    result = _build_validation_export(session, ArtifactFormat.MARKDOWN)
    assert result["storage_ref"].endswith("validation.md")
    assert "# Validation Report:" in result["content"]["markdown"]
    assert "TEST-001" in result["content"]["markdown"]
#endregion


#region test_build_validation_export_no_findings [C:2]
# @BRIEF Markdown handles no findings in validation.
def test_build_validation_export_no_findings():
    session = _make_export_session_no_profile()
    result = _build_validation_export(session, ArtifactFormat.MARKDOWN)
    assert "No findings recorded" in result["content"]["markdown"]
#endregion


# ── Serialization helpers ──────────────────────────────────────────────


#region test_serialize_session_summary [C:2]
# @BRIEF Maps session to SessionSummary DTO with session_version.
def test_serialize_session_summary():
    session = _make_session_mock(version=7)
    result = _serialize_session_summary(session)
    assert isinstance(result, SessionSummary)
    assert result.session_id == "sess-1"
    assert result.session_version == 7
    assert result.version == 7
#endregion


#region test_serialize_session_detail [C:2]
# @BRIEF Maps session to SessionDetail DTO with nested collections.
def test_serialize_session_detail():
    session = _make_session_mock(version=3)
    result = _serialize_session_detail(session)
    assert isinstance(result, SessionDetail)
    assert result.session_id == "sess-1"
    assert result.session_version == 3
    assert result.version == 3
    assert result.findings == []
    assert result.semantic_fields == []
#endregion


#region test_serialize_semantic_field [C:2]
# @BRIEF Maps semantic field to SemanticFieldEntryDto with session version.
def test_serialize_semantic_field():
    field = MagicMock()
    field.field_id = "field-1"
    field.session_id = "sess-1"
    field.field_name = "revenue"
    field.field_kind = FieldKind.METRIC
    field.verbose_name = "Revenue"
    field.description = "Total revenue"
    field.display_format = "$,.0f"
    field.provenance = FieldProvenance.DICTIONARY_EXACT
    field.source_id = None
    field.source_version = None
    field.confidence_rank = 1
    field.is_locked = False
    field.has_conflict = False
    field.needs_review = False
    field.last_changed_by = "user"
    field.user_feedback = None
    field.created_at = datetime.now(UTC)
    field.updated_at = datetime.now(UTC)
    field.candidates = []
    session = MagicMock(version=4)
    field.session = session
    result = _serialize_semantic_field(field)
    assert result.field_id == "field-1"
    assert result.session_version == 4
#endregion


#region test_serialize_semantic_field_no_session [C:2]
# @BRIEF Handles field without session reference.
def test_serialize_semantic_field_no_session():
    field = MagicMock()
    field.field_id = "field-1"
    field.session_id = "sess-1"
    field.field_name = "revenue"
    field.field_kind = FieldKind.METRIC
    field.verbose_name = "Revenue"
    field.description = "Total revenue"
    field.display_format = "$,.0f"
    field.provenance = FieldProvenance.DICTIONARY_EXACT
    field.source_id = None
    field.source_version = None
    field.confidence_rank = None
    field.is_locked = False
    field.has_conflict = False
    field.needs_review = False
    field.last_changed_by = "system"
    field.user_feedback = None
    field.created_at = datetime.now(UTC)
    field.updated_at = datetime.now(UTC)
    field.candidates = []
    field.session = None
    result = _serialize_semantic_field(field)
    assert result.session_version is None
#endregion


#region test_serialize_execution_mapping [C:2]
# @BRIEF Maps execution mapping to ExecutionMappingDto with session version.
def test_serialize_execution_mapping():
    mapping = MagicMock()
    mapping.mapping_id = "map-1"
    mapping.session_id = "sess-1"
    mapping.filter_id = "fl-1"
    mapping.variable_id = "var-1"
    mapping.mapping_method = MappingMethod.DIRECT_MATCH
    mapping.raw_input_value = "DE"
    mapping.effective_value = "DE"
    mapping.transformation_note = None
    mapping.warning_level = None
    mapping.requires_explicit_approval = False
    mapping.approval_state = ApprovalState.APPROVED
    mapping.approved_by_user_id = None
    mapping.approved_at = None
    mapping.created_at = datetime.now(UTC)
    mapping.updated_at = datetime.now(UTC)
    mapping.session = MagicMock(version=5)
    result = _serialize_execution_mapping(mapping)
    assert isinstance(result, ExecutionMappingDto)
    assert result.mapping_id == "map-1"
    assert result.session_version == 5
#endregion


#region test_serialize_preview [C:2]
# @BRIEF Maps preview to CompiledPreviewDto with session version fallback.
def test_serialize_preview():
    preview = MagicMock()
    preview.preview_id = "prev-1"
    preview.session_id = "sess-1"
    preview.preview_status = PreviewStatus.READY
    preview.compiled_sql = "SELECT 1"
    preview.preview_fingerprint = "fp-1"
    preview.compiled_by = "system"
    preview.error_code = None
    preview.error_details = None
    preview.compiled_at = datetime.now(UTC)
    preview.created_at = datetime.now(UTC)
    preview.session = MagicMock(version=6)
    result = _serialize_preview(preview)
    assert isinstance(result, CompiledPreviewDto)
    assert result.preview_id == "prev-1"
    assert result.session_version == 6
#endregion


#region test_serialize_preview_fallback [C:2]
# @BRIEF Uses session_version_fallback when preview has no session reference.
def test_serialize_preview_fallback():
    preview = MagicMock()
    preview.preview_id = "prev-1"
    preview.session_id = "sess-1"
    preview.preview_status = PreviewStatus.PENDING
    preview.compiled_sql = None
    preview.preview_fingerprint = "fp-1"
    preview.compiled_by = "system"
    preview.error_code = None
    preview.error_details = None
    preview.compiled_at = None
    preview.created_at = datetime.now(UTC)
    preview.session = None
    result = _serialize_preview(preview, session_version_fallback=3)
    assert result.session_version == 3
#endregion


#region test_serialize_run_context [C:2]
# @BRIEF Maps run context to DatasetRunContextDto with session version.
def test_serialize_run_context():
    rc = MagicMock()
    rc.run_context_id = "rc-1"
    rc.session_id = "sess-1"
    rc.dataset_ref = "public.sales"
    rc.environment_id = "env-1"
    rc.preview_id = "prev-1"
    rc.sql_lab_session_ref = "sql-lab-1"
    rc.effective_filters = []
    rc.template_params = {}
    rc.approved_mapping_ids = []
    rc.semantic_decision_refs = []
    rc.open_warning_refs = []
    rc.launch_status = LaunchStatus.STARTED
    rc.launch_error = None
    rc.created_at = datetime.now(UTC)
    rc.session = MagicMock(version=7)
    result = _serialize_run_context(rc)
    assert result.run_context_id == "rc-1"
    assert result.session_version == 7
    assert result.dataset_ref == "public.sales"
#endregion


#region test_serialize_clarification_question_payload [C:2]
# @BRIEF Converts ClarificationQuestionPayload into DTO.
def test_serialize_clarification_question_payload():
    payload = MagicMock()
    payload.question_id = "q-1"
    payload.clarification_session_id = "cs-1"
    payload.topic_ref = "topic"
    payload.question_text = "What is this?"
    payload.why_it_matters = "It matters because..."
    payload.current_guess = "Guess"
    payload.priority = 1
    payload.state = QuestionState.OPEN
    payload.options = []
    result = _serialize_clarification_question_payload(payload)
    assert result is not None
    assert result.question_id == "q-1"
    assert result.question_text == "What is this?"
    assert result.options == []
#endregion


#region test_serialize_clarification_question_payload_none [C:2]
# @BRIEF Returns None when payload is None.
def test_serialize_clarification_question_payload_none():
    result = _serialize_clarification_question_payload(None)
    assert result is None
#endregion


#region test_serialize_clarification_state [C:2]
# @BRIEF Converts ClarificationStateResult into API response.
def test_serialize_clarification_state():
    state = MagicMock()
    cs = MagicMock()
    cs.clarification_session_id = "cs-1"
    cs.session_id = "sess-1"
    cs.status = ClarificationStatus.ACTIVE
    cs.current_question_id = "q-1"
    cs.resolved_count = 2
    cs.remaining_count = 3
    cs.summary_delta = "Progressing"
    state.clarification_session = cs

    question_payload = MagicMock()
    question_payload.question_id = "q-1"
    question_payload.clarification_session_id = "cs-1"
    question_payload.topic_ref = "topic"
    question_payload.question_text = "Question?"
    question_payload.why_it_matters = "Because"
    question_payload.current_guess = None
    question_payload.priority = 1
    question_payload.state = QuestionState.OPEN
    question_payload.options = []
    state.current_question = question_payload

    result = _serialize_clarification_state(state)
    assert result.clarification_session is not None
    assert result.clarification_session.clarification_session_id == "cs-1"
    assert result.clarification_session.resolved_count == 2
    assert result.current_question is not None
    assert result.current_question.question_id == "q-1"
#endregion


#region test_serialize_empty_clarification_state [C:2]
# @BRIEF Returns empty state with None values.
def test_serialize_empty_clarification_state():
    result = _serialize_empty_clarification_state()
    assert result.clarification_session is None
    assert result.current_question is None
#endregion


# ── Dependency injection functions ──────────────────────────────────────


#region test_get_repository [C:2]
# @BRIEF _get_repository wraps a Session into DatasetReviewSessionRepository.
def test_get_repository():
    mock_db = MagicMock()
    repo = _get_repository(db=mock_db)
    from src.services.dataset_review.repositories.session_repository import (
        DatasetReviewSessionRepository,
    )
    assert isinstance(repo, DatasetReviewSessionRepository)
    assert repo.db is mock_db
#endregion


#region test_get_orchestrator [C:2]
# @BRIEF _get_orchestrator builds DatasetReviewOrchestrator with deps.
def test_get_orchestrator():
    mock_repo = MagicMock()
    mock_cm = MagicMock()
    mock_tm = MagicMock()
    orch = _get_orchestrator(repository=mock_repo, config_manager=mock_cm, task_manager=mock_tm)
    from src.services.dataset_review.orchestrator import DatasetReviewOrchestrator
    assert isinstance(orch, DatasetReviewOrchestrator)
    assert orch.repository is mock_repo
#endregion


#region test_get_clarification_engine [C:2]
# @BRIEF _get_clarification_engine builds ClarificationEngine with repo.
def test_get_clarification_engine():
    mock_repo = MagicMock()
    engine = _get_clarification_engine(repository=mock_repo)
    from src.services.dataset_review.clarification_engine import ClarificationEngine
    assert isinstance(engine, ClarificationEngine)
    assert engine.repository is mock_repo
#endregion


#endregion Test.DatasetReview.DepsUnit
