# #region Test.DatasetReview.SessionLifecycle [C:3] [TYPE Module] [SEMANTICS test,dataset,review,sessions,lifecycle]
# @BRIEF Session lifecycle tests for dataset review routes — covers list, update, delete, export, batch, mapping & feedback.
# @RELATION BINDS_TO -> [DatasetReviewRoutes]
# @TEST_EDGE: list_sessions_pagination -> paginated listing returns correct items/total/has_next
# @TEST_EDGE: update_session_pause -> PATCH session status to PAUSED sets recommended_action=RESUME_SESSION
# @TEST_EDGE: update_session_archive -> PATCH session to ARCHIVED clears active_task_id
# @TEST_EDGE: delete_session_hard -> DELETE hard_delete removes from DB
# @TEST_EDGE: delete_session_archive -> DELETE without hard_delete archives
# @TEST_EDGE: export_validation_markdown -> export validation in markdown format
# @TEST_EDGE: batch_approve_semantic -> batch approve semantic fields
# @TEST_EDGE: update_execution_mapping_full -> full execution mapping update
# @TEST_EDGE: approve_execution_mapping_with_note -> approve mapping with note
# @TEST_EDGE: batch_approve_mappings -> batch approve mappings
# @TEST_EDGE: record_field_feedback -> persist field feedback
# @TEST_EDGE: record_clarification_feedback -> persist clarification feedback
from __future__ import annotations

from datetime import UTC, datetime
from pathlib import Path
import sys
from types import SimpleNamespace

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

import pytest
from unittest.mock import MagicMock, patch

from fastapi.testclient import TestClient
from fastapi import status

from src.api.routes.dataset_review import (
    _get_orchestrator,
    _get_repository,
    _get_clarification_engine,
)
from src.app import app
from src.core.config_models import AppConfig, Environment, GlobalSettings
from src.dependencies import get_config_manager, get_current_user, get_task_manager
from src.models.dataset_review import (
    ApprovalState,
    ArtifactFormat,
    CandidateMatchType,
    CandidateStatus,
    FieldKind,
    MappingMethod,
    PreviewStatus,
    ReadinessState,
    RecommendedAction,
    SessionStatus,
    FieldProvenance,
)
from src.models.auth import User
from src.services.dataset_review.event_logger import SessionEventLogger
from src.services.dataset_review.orchestrator import (
    DatasetReviewOrchestrator,
    LaunchDatasetResult,
    PreparePreviewResult,
    StartSessionCommand,
    StartSessionResult,
)
from src.services.dataset_review.repositories.session_repository import (
    DatasetReviewSessionVersionConflictError,
)

client = TestClient(app)

# ── Shared fixture: cleanup ───────────────────────────────────


@pytest.fixture(autouse=True)
def _clean_overrides():
    app.dependency_overrides.clear()
    yield
    app.dependency_overrides.clear()


# ── Builders ──────────────────────────────────────────────────


def _make_user():
    permissions = [
        SimpleNamespace(resource="dataset:session", action="READ"),
        SimpleNamespace(resource="dataset:session", action="MANAGE"),
        SimpleNamespace(resource="dataset:execution:launch", action="EXECUTE"),
    ]
    role = SimpleNamespace(name="DatasetReviewOperator", permissions=permissions)
    return SimpleNamespace(id="user-1", username="tester", roles=[role])


def _make_config_manager(ff_auto=True, ff_clarification=True, ff_execution=True):
    env = Environment(
        id="env-1", name="DEV", url="http://superset.local",
        username="demo", password="secret",
    )
    config = AppConfig(environments=[env], settings=GlobalSettings())
    config.settings.features.dataset_review = True
    config.settings.ff_dataset_auto_review = ff_auto
    config.settings.ff_dataset_clarification = ff_clarification
    config.settings.ff_dataset_execution = ff_execution
    manager = MagicMock()
    manager.get_environment.side_effect = (
        lambda env_id: env if env_id == "env-1" else None
    )
    manager.get_config.return_value = config
    return manager


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


def _make_candidate(candidate_id="cand-1", **kw):
    """Build a semantic candidate mock with all SemanticCandidateDto fields."""
    c = MagicMock()
    c.candidate_id = candidate_id
    c.field_id = kw.get("field_id", "field-1")
    c.source_id = kw.get("source_id", "source-1")
    c.candidate_rank = kw.get("candidate_rank", 1)
    c.match_type = kw.get("match_type", CandidateMatchType.GENERATED)
    c.confidence_score = kw.get("confidence_score", 0.95)
    c.proposed_verbose_name = kw.get("proposed_verbose_name", "Field Name")
    c.proposed_description = kw.get("proposed_description", "Description")
    c.proposed_display_format = kw.get("proposed_display_format", None)
    c.status = kw.get("status", CandidateStatus.PROPOSED)
    c.created_at = datetime.now(UTC)
    return c


def _make_field(field_id="field-1", **kw):
    f = MagicMock()
    f.field_id = field_id
    f.session_id = kw.get("session_id", "sess-1")
    f.field_name = kw.get("field_name", field_id)
    f.field_kind = kw.get("field_kind", FieldKind.COLUMN)
    f.is_locked = kw.get("is_locked", False)
    f.provenance = kw.get("provenance", FieldProvenance.AI_GENERATED)
    f.last_changed_by = "system"
    f.needs_review = kw.get("needs_review", True)
    f.has_conflict = kw.get("has_conflict", False)
    f.user_feedback = None
    f.created_at = datetime.now(UTC)
    f.updated_at = datetime.now(UTC)
    f.candidates = kw.get("candidates", [])
    return f


def _make_mapping(mapping_id="map-1", **kw):
    m = MagicMock()
    m.mapping_id = mapping_id
    m.session_id = kw.get("session_id", "sess-1")
    m.filter_id = kw.get("filter_id", "filt-1")
    m.variable_id = kw.get("variable_id", "var-1")
    m.raw_input_value = kw.get("raw_input_value", "raw_val")
    m.requires_explicit_approval = kw.get("requires_explicit_approval", False)
    m.effective_value = kw.get("effective_value", "old_value")
    m.mapping_method = MappingMethod.SEMANTIC_MATCH
    m.transformation_note = None
    m.approval_state = kw.get("approval_state", ApprovalState.PENDING)
    m.approved_by_user_id = None
    m.approved_at = None
    m.preferred_name = kw.get("preferred_name", "field")
    m.source_field = "src_field"
    m.target_field = "tgt_field"
    m.warning_level = None
    m.created_at = datetime.now(UTC)
    m.updated_at = datetime.now(UTC)
    return m


def _make_clarification_session(**kw):
    cs = MagicMock()
    cs.questions = kw.get("questions", [])
    return cs


def _make_question(question_id="q-1", **kw):
    q = MagicMock()
    q.question_id = question_id
    q.answer = kw.get("answer", None)
    return q


def _make_answer(**kw):
    a = MagicMock()
    a.user_feedback = kw.get("user_feedback", None)
    return a


def _setup_deps(repository=None, orchestrator=None, clarification_engine=None,
                 session=None, config_manager=None, user=None):
    user = user or _make_user()
    cm = config_manager or _make_config_manager()
    effective_session = session or _make_session()

    if repository is None:
        repository = MagicMock()
        repository.load_session_detail.return_value = effective_session
        repository.list_sessions_for_user.return_value = [effective_session]
        repository.db = MagicMock()
        repository.event_logger = MagicMock(spec=SessionEventLogger)
        repository.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )

    if orchestrator is None:
        orchestrator = MagicMock()
        orchestrator.start_session.return_value = StartSessionResult(
            session=effective_session
        )

    app.dependency_overrides[get_config_manager] = lambda: cm
    app.dependency_overrides[get_current_user] = lambda: user
    app.dependency_overrides[_get_repository] = lambda: repository
    app.dependency_overrides[_get_orchestrator] = lambda: orchestrator
    if clarification_engine is not None:
        app.dependency_overrides[_get_clarification_engine] = lambda: clarification_engine

    return {"repository": repository, "orchestrator": orchestrator}


# ── list_sessions ─────────────────────────────────────────────


class TestListSessions:
    """GET /api/dataset-orchestration/sessions"""

    # #region test_list_sessions_paginated [C:2] [TYPE Function]
    def test_list_sessions_paginated(self):
        s1 = _make_session(session_id="sess-1")
        s2 = _make_session(session_id="sess-2")
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [s1, s2]
        _setup_deps(repository=repo)

        resp = client.get("/api/dataset-orchestration/sessions?page=1&page_size=1")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 2
        assert len(data["items"]) == 1
        assert data["page"] == 1
        assert data["page_size"] == 1
        assert data["has_next"] is True

    # #endregion test_list_sessions_paginated

    # #region test_list_sessions_last_page [C:2] [TYPE Function]
    def test_list_sessions_last_page(self):
        s1 = _make_session(session_id="sess-1")
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [s1]
        _setup_deps(repository=repo)

        resp = client.get("/api/dataset-orchestration/sessions?page=1&page_size=20")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 1
        assert data["has_next"] is False
    # #endregion test_list_sessions_last_page


# ── update_session ────────────────────────────────────────────


class TestUpdateSession:
    """PATCH /api/dataset-orchestration/sessions/{session_id}"""

    # #region test_update_session_pause [C:2] [TYPE Function]
    def test_update_session_pause(self):
        session = _make_session(version=1)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.bump_session_version = MagicMock()
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.patch(
            "/api/dataset-orchestration/sessions/sess-1",
            json={"status": "paused"},
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data is not None
    # #endregion test_update_session_pause

    # #region test_update_session_archive [C:2] [TYPE Function]
    def test_update_session_archive(self):
        session = _make_session(version=1)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.bump_session_version = MagicMock()
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.patch(
            "/api/dataset-orchestration/sessions/sess-1",
            json={"status": "archived"},
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
    # #endregion test_update_session_archive


# ── delete_session ────────────────────────────────────────────


class TestDeleteSession:
    """DELETE /api/dataset-orchestration/sessions/{session_id}"""

    # #region test_delete_session_hard [C:2] [TYPE Function]
    def test_delete_session_hard(self):
        session = _make_session(version=1)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.require_session_version = MagicMock(return_value=session)
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.delete(
            "/api/dataset-orchestration/sessions/sess-1?hard_delete=true",
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 204
    # #endregion test_delete_session_hard


# ── export_validation ─────────────────────────────────────────


class TestExportValidation:
    """GET /api/dataset-orchestration/sessions/{session_id}/exports/validation"""

    # #region test_export_validation_json [C:2] [TYPE Function]
    def test_export_validation_json(self):
        session = _make_session()
        _setup_deps(session=session)

        resp = client.get(
            "/api/dataset-orchestration/sessions/sess-1/exports/validation?format=json"
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["artifact_type"] == "validation_report"
    # #endregion test_export_validation_json

    # #region test_export_validation_markdown [C:2] [TYPE Function]
    def test_export_validation_markdown(self):
        session = _make_session()
        _setup_deps(session=session)

        resp = client.get(
            "/api/dataset-orchestration/sessions/sess-1/exports/validation?format=markdown"
        )
        assert resp.status_code == 200
    # #endregion test_export_validation_markdown


# ── batch_approve_semantic_fields ─────────────────────────────


class TestBatchApproveSemantic:
    """POST /api/dataset-orchestration/sessions/{session_id}/fields/semantic/approve-batch"""

    # #region test_batch_approve_semantic [C:2] [TYPE Function]
    def test_batch_approve_semantic(self):
        cand1 = _make_candidate(candidate_id="cand-1", field_id="field-1")
        cand2 = _make_candidate(candidate_id="cand-2", field_id="field-2")
        field1 = _make_field(field_id="field-1", candidates=[cand1])
        field2 = _make_field(field_id="field-2", candidates=[cand2])
        session = _make_session(semantic_fields=[field1, field2], version=1)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.post(
            "/api/dataset-orchestration/sessions/sess-1/fields/semantic/approve-batch",
            json={
                "items": [
                    {"field_id": "field-1", "candidate_id": "cand-1", "lock_field": True},
                    {"field_id": "field-2", "candidate_id": "cand-2", "lock_field": False},
                ]
            },
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data, list)
    # #endregion test_batch_approve_semantic


# ── update_execution_mapping ──────────────────────────────────


class TestUpdateExecutionMapping:
    """PATCH /api/dataset-orchestration/sessions/{session_id}/mappings/{mapping_id}"""

    # #region test_update_execution_mapping [C:2] [TYPE Function]
    def test_update_execution_mapping(self):
        mapping = _make_mapping(mapping_id="map-1")
        session = _make_session(execution_mappings=[mapping], version=1,
                                readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.patch(
            "/api/dataset-orchestration/sessions/sess-1/mappings/map-1",
            json={
                "effective_value": "new_value",
                "mapping_method": "manual_override",
                "transformation_note": "Updated by user",
            },
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["mapping_id"] == "map-1"
    # #endregion test_update_execution_mapping


# ── approve_execution_mapping ─────────────────────────────────


class TestApproveExecutionMapping:
    """POST /api/dataset-orchestration/sessions/{session_id}/mappings/{mapping_id}/approve"""

    # #region test_approve_mapping_with_note [C:2] [TYPE Function]
    def test_approve_mapping_with_note(self):
        mapping = _make_mapping(mapping_id="map-1")
        session = _make_session(execution_mappings=[mapping], version=1,
                                readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.post(
            "/api/dataset-orchestration/sessions/sess-1/mappings/map-1/approve",
            json={"approval_note": "Looks good"},
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
    # #endregion test_approve_mapping_with_note


# ── approve_batch_execution_mappings ──────────────────────────


class TestBatchApproveMappings:
    """POST /api/dataset-orchestration/sessions/{session_id}/mappings/approve-batch"""

    # #region test_batch_approve_mappings [C:2] [TYPE Function]
    def test_batch_approve_mappings(self):
        mapping1 = _make_mapping(mapping_id="map-1")
        mapping2 = _make_mapping(mapping_id="map-2")
        session = _make_session(execution_mappings=[mapping1, mapping2], version=1,
                                readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.post(
            "/api/dataset-orchestration/sessions/sess-1/mappings/approve-batch",
            json={"mapping_ids": ["map-1", "map-2"], "approval_note": "Batch approved"},
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data, list)
        assert len(data) == 2
    # #endregion test_batch_approve_mappings


# ── record_field_feedback ─────────────────────────────────────


class TestRecordFieldFeedback:
    """POST /api/dataset-orchestration/sessions/{session_id}/fields/{field_id}/feedback"""

    # #region test_field_feedback_thumbs_up [C:2] [TYPE Function]
    def test_field_feedback_thumbs_up(self):
        field = _make_field(field_id="field-1")
        session = _make_session(semantic_fields=[field], version=1)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.post(
            "/api/dataset-orchestration/sessions/sess-1/fields/field-1/feedback",
            json={"feedback": "up"},
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["feedback"] == "up"
        assert data["target_id"] == "field-1"
    # #endregion test_field_feedback_thumbs_up


# ── record_clarification_feedback ─────────────────────────────


class TestRecordClarificationFeedback:
    """POST /api/dataset-orchestration/sessions/{session_id}/clarification/questions/{question_id}/feedback"""

    # #region test_clarification_feedback_success [C:2] [TYPE Function]
    def test_clarification_feedback_success(self):
        answer = _make_answer()
        question = _make_question(question_id="q-1", answer=answer)
        cs = _make_clarification_session(questions=[question])
        session = _make_session(clarification_sessions=[cs], version=1)
        repo = MagicMock()
        repo.list_sessions_for_user.return_value = [session]
        repo.load_session_detail.return_value = session
        repo.require_session_version = MagicMock(return_value=session)
        repo.commit_session_mutation = MagicMock()
        repo.db = MagicMock()
        repo.event_logger = MagicMock(spec=SessionEventLogger)
        repo.event_logger.log_for_session.return_value = SimpleNamespace(
            session_event_id="evt-0"
        )
        _setup_deps(repository=repo, session=session)

        resp = client.post(
            "/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-1/feedback",
            json={"feedback": "down"},
            headers={"X-Session-Version": "1"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["feedback"] == "down"
    # #endregion test_clarification_feedback_success
# #endregion Test.DatasetReview.SessionLifecycle
