# #region Test.DatasetReview.SessionRepository [C:4] [TYPE Module] [SEMANTICS test, session, repository, dataset, review]
# @BRIEF Tests for DatasetReviewSessionRepository — persistence and retrieval.
# @RELATION BINDS_TO -> [DatasetReviewSessionRepository]

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

from datetime import UTC, datetime
from unittest.mock import MagicMock, patch

import pytest
from sqlalchemy.orm.exc import StaleDataError
from sqlalchemy.orm import Session

from src.services.dataset_review.repositories.session_repository import (
    DatasetReviewSessionRepository,
    DatasetReviewSessionVersionConflictError,
)
from src.models.dataset_review import (
    DatasetReviewSession,
    DatasetProfile,
    ValidationFinding,
    CompiledPreview,
    DatasetRunContext,
    ImportedFilter,
    TemplateVariable,
    ExecutionMapping,
    SessionCollaborator,
    SemanticFieldEntry,
    ClarificationSession,
    ClarificationQuestion,
)


@pytest.fixture
def mock_db():
    return MagicMock(spec=Session)


@pytest.fixture
def repo(mock_db):
    return DatasetReviewSessionRepository(mock_db)


class TestInit:
    def test_creates_event_logger(self, mock_db):
        repo = DatasetReviewSessionRepository(mock_db)
        assert repo.db is mock_db
        assert repo.event_logger is not None


class TestGetOwnedSession:
    def test_found(self, repo, mock_db):
        mock_session = MagicMock(spec=DatasetReviewSession)
        mock_db.query.return_value.filter.return_value.first.return_value = mock_session
        result = repo._get_owned_session("s1", "u1")
        assert result is mock_session

    def test_not_found_raises(self, repo, mock_db):
        mock_db.query.return_value.filter.return_value.first.return_value = None
        with pytest.raises(ValueError, match="Session not found"):
            repo._get_owned_session("s1", "u1")


class TestCreateSession:
    def test_creates_and_refreshes(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        result = repo.create_session(session)
        mock_db.add.assert_called_once_with(session)
        mock_db.commit.assert_called_once()
        mock_db.refresh.assert_called_once_with(session)
        assert result is session


class TestRequireSessionVersion:
    def test_version_matches(self, repo):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 3
        result = repo.require_session_version(session, 3)
        assert result is session

    def test_version_conflict(self, repo):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 2
        with pytest.raises(DatasetReviewSessionVersionConflictError) as exc:
            repo.require_session_version(session, 5)
        assert exc.value.expected_version == 5
        assert exc.value.actual_version == 2

    def test_version_none(self, repo):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = None
        result = repo.require_session_version(session, 0)
        assert result is session


class TestBumpSessionVersion:
    def test_bumps_version(self, repo):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 2
        result = repo.bump_session_version(session)
        assert result == 3
        assert session.version == 3

    def test_bumps_from_none(self, repo):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = None
        result = repo.bump_session_version(session)
        assert result == 1
        assert session.version == 1


class TestCommitSessionMutation:
    def test_commit_success_no_refresh_targets(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 1
        result = repo.commit_session_mutation(session)
        mock_db.commit.assert_called_once()
        mock_db.refresh.assert_called_once_with(session)
        assert result is session

    def test_commit_with_refresh_targets(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 1
        target = MagicMock()
        result = repo.commit_session_mutation(session, refresh_targets=[target])
        assert mock_db.refresh.call_count == 2  # session + target
        assert result is session

    def test_commit_stale_data_error(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 2
        session.session_id = "s1"
        mock_db.commit.side_effect = StaleDataError("stale")
        mock_db.query.return_value.filter.return_value.first.return_value = (5,)
        with pytest.raises(DatasetReviewSessionVersionConflictError) as exc:
            repo.commit_session_mutation(session, expected_version=2)
        mock_db.rollback.assert_called_once()
        assert exc.value.expected_version == 2
        assert exc.value.actual_version == 5

    def test_commit_stale_no_row(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 2
        session.session_id = "s1"
        mock_db.commit.side_effect = StaleDataError("stale")
        mock_db.query.return_value.filter.return_value.first.return_value = None
        with pytest.raises(DatasetReviewSessionVersionConflictError) as exc:
            repo.commit_session_mutation(session, expected_version=2)
        assert exc.value.actual_version == 0

    def test_commit_custom_expected_version(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        session.version = 99
        result = repo.commit_session_mutation(session, expected_version=5)
        # bump goes from 99 to 100, but expected_version=5 is used for error reporting
        mock_db.commit.assert_called_once()
        assert result is session


class TestLoadSessionDetail:
    def test_found(self, repo, mock_db):
        mock_session = MagicMock(spec=DatasetReviewSession)
        mock_db.query.return_value.outerjoin.return_value.options.return_value.filter.return_value.filter.return_value.first.return_value = mock_session
        result = repo.load_session_detail("s1", "u1")
        assert result is mock_session

    def test_not_found(self, repo, mock_db):
        mock_db.query.return_value.outerjoin.return_value.options.return_value.filter.return_value.filter.return_value.first.return_value = None
        result = repo.load_session_detail("s1", "u1")
        assert result is None


class TestSaveDelegates:
    def test_save_profile_and_findings(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        mock_db.query.return_value.filter.return_value.first.return_value = session
        profile = MagicMock(spec=DatasetProfile)
        findings = [MagicMock(spec=ValidationFinding)]
        with patch("src.services.dataset_review.repositories.repository_pkg._mutations.save_profile_and_findings") as mock_save:
            mock_save.return_value = session
            result = repo.save_profile_and_findings("s1", "u1", profile, findings)
            mock_save.assert_called_once()
            assert result is session

    def test_save_recovery_state(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        mock_db.query.return_value.filter.return_value.first.return_value = session
        imported_filters = [MagicMock(spec=ImportedFilter)]
        template_variables = [MagicMock(spec=TemplateVariable)]
        execution_mappings = [MagicMock(spec=ExecutionMapping)]
        with patch("src.services.dataset_review.repositories.repository_pkg._mutations.save_recovery_state") as mock_save:
            mock_save.return_value = session
            result = repo.save_recovery_state("s1", "u1", imported_filters, template_variables, execution_mappings)
            mock_save.assert_called_once()
            assert result is session

    def test_save_preview(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        mock_db.query.return_value.filter.return_value.first.return_value = session
        preview = MagicMock(spec=CompiledPreview)
        with patch("src.services.dataset_review.repositories.repository_pkg._mutations.save_preview") as mock_save:
            mock_save.return_value = preview
            result = repo.save_preview("s1", "u1", preview)
            mock_save.assert_called_once()
            assert result is preview

    def test_save_run_context(self, repo, mock_db):
        session = MagicMock(spec=DatasetReviewSession)
        mock_db.query.return_value.filter.return_value.first.return_value = session
        run_context = MagicMock(spec=DatasetRunContext)
        with patch("src.services.dataset_review.repositories.repository_pkg._mutations.save_run_context") as mock_save:
            mock_save.return_value = run_context
            result = repo.save_run_context("s1", "u1", run_context)
            mock_save.assert_called_once()
            assert result is run_context


class TestListSessionsForUser:
    def test_returns_list(self, repo, mock_db):
        mock_sessions = [MagicMock(spec=DatasetReviewSession), MagicMock(spec=DatasetReviewSession)]
        mock_db.query.return_value.filter.return_value.order_by.return_value.all.return_value = mock_sessions
        result = repo.list_sessions_for_user("u1")
        assert len(result) == 2
        assert result == mock_sessions

    def test_empty(self, repo, mock_db):
        mock_db.query.return_value.filter.return_value.order_by.return_value.all.return_value = []
        result = repo.list_sessions_for_user("u1")
        assert result == []


class TestVersionConflictError:
    def test_str(self):
        exc = DatasetReviewSessionVersionConflictError("s1", 1, 5)
        assert str(exc) == "Session version conflict: expected 1, actual 5"
        assert exc.session_id == "s1"
        assert exc.expected_version == 1
        assert exc.actual_version == 5
# #endregion Test.DatasetReview.SessionRepository
