# #region Test.RunExecutionService [C:3] [TYPE Module] [SEMANTICS test, translate, run, execution]
# @BRIEF Tests for _run_service.py — RunExecutionService.
# @RELATION BINDS_TO -> [_run_service]
# @TEST_EDGE: filter_new_keys_no_prev_run -> all keys treated as new
# @TEST_EDGE: filter_new_keys_no_key_cols -> all rows pass through
# @TEST_EDGE: load_preview_edits -> edits loaded from session
import pytest
from unittest.mock import MagicMock, patch
from datetime import UTC, datetime
import uuid

from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, sessionmaker

from src.models.translate import (
    Base, TranslationBatch, TranslationJob, TranslationRun, TranslationRecord,
    TranslationLanguage, TranslationRunLanguageStats,
    TranslationPreviewSession, TranslationPreviewRecord,
)


def make_session():
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(bind=engine)
    session = sessionmaker(bind=engine)()
    return session, engine


class TestRunExecutionService:
    """RunExecutionService — orchestrate full translation run."""

    def _make_service(self, db, **kwargs):
        from src.plugins.translate._run_service import RunExecutionService
        config_manager = kwargs.pop("config_manager", MagicMock())
        return RunExecutionService(db, config_manager, **kwargs)

    # -- _fetch_source_rows / _extract_chart_data_rows (thin wrappers) --

    @pytest.mark.asyncio
    async def test_fetch_source_rows_calls_imported(self):
        """_fetch_source_rows delegates to module-level fetch_source_rows."""
        session, engine = make_session()
        try:
            svc = self._make_service(session)
            from src.models.translate import TranslationJob
            job = TranslationJob(id="fsr-job", name="fsr", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr")
            session.add(job)
            session.commit()

            with patch("src.plugins.translate._run_service.fetch_source_rows",
                       return_value=[{"id": 1}]) as mock_fetch:
                result = await svc._fetch_source_rows("fsr-job", "run-fsr-1")
                assert result == [{"id": 1}]
                mock_fetch.assert_called_once_with(
                    session, svc.config_manager, "fsr-job", "run-fsr-1"
                )
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_extract_chart_data_rows_delegates(self):
        """Static _extract_chart_data_rows delegates to module-level function."""
        from src.plugins.translate._run_service import RunExecutionService
        with patch("src.plugins.translate._run_service._extract_chart_data_rows",
                   return_value=[{"id": 1}]) as mock_extract:
            result = RunExecutionService._extract_chart_data_rows({"data": []})
            assert result == [{"id": 1}]
            mock_extract.assert_called_once_with({"data": []})

    # -- _filter_new_keys --

    def test_filter_new_keys_no_prev_run(self):
        """No prior completed run -> all source rows treated as new."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j1", name="j1", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr")
            session.add(job)
            session.commit()
            svc = self._make_service(session)
            source_rows = [{"source_data": {"id": 1}}]
            result = svc._filter_new_keys(job, "run-1", source_rows)
            assert len(result) == 1
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_filter_new_keys_with_prev_run_filters(self):
        """Prior completed run -> rows with existing keys filtered out."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j2", name="j2", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr",
                                 target_key_cols=["id"])
            session.add(job)
            prev_run = TranslationRun(id="prev-run", job_id="j2", status="COMPLETED",
                                      insert_status="succeeded",
                                      started_at=datetime.now(UTC),
                                      created_at=datetime.now(UTC))
            session.add(prev_run)
            session.flush()
            batch = TranslationBatch(id="batch-prev", run_id="prev-run", batch_index=0,
                                     status="COMPLETED", created_at=datetime.now(UTC))
            session.add(batch)
            session.flush()
            # Record with existing key
            rec = TranslationRecord(id="rec-1", run_id="prev-run", batch_id="batch-prev",
                                    status="SUCCESS", source_data={"id": 1})
            session.add(rec)
            session.commit()

            svc = self._make_service(session)
            source_rows = [
                {"source_data": {"id": 1}},  # already exists
                {"source_data": {"id": 2}},  # new
            ]
            result = svc._filter_new_keys(job, "curr-run", source_rows)
            assert len(result) == 1
            assert result[0]["source_data"]["id"] == 2
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_filter_new_keys_no_key_cols(self):
        """No key cols configured -> all rows pass through."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j3", name="j3", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr",
                                 target_key_cols=None, source_key_cols=None)
            session.add(job)
            prev_run = TranslationRun(id="prev-run-3", job_id="j3", status="COMPLETED",
                                      insert_status="succeeded",
                                      started_at=datetime.now(UTC),
                                      created_at=datetime.now(UTC))
            session.add(prev_run)
            session.commit()

            svc = self._make_service(session)
            source_rows = [{"source_data": {"id": 1}}, {"source_data": {"id": 2}}]
            result = svc._filter_new_keys(job, "curr-run-3", source_rows)
            assert len(result) == 2
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_filter_new_keys_no_prev_records(self):
        """Prior run has no records -> all rows new."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j4", name="j4", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr",
                                 target_key_cols=["id"])
            session.add(job)
            prev_run = TranslationRun(id="prev-run-4", job_id="j4", status="COMPLETED",
                                      insert_status="succeeded",
                                      started_at=datetime.now(UTC),
                                      created_at=datetime.now(UTC))
            session.add(prev_run)
            session.commit()

            svc = self._make_service(session)
            source_rows = [{"source_data": {"id": 1}}]
            result = svc._filter_new_keys(job, "curr-run-4", source_rows)
            assert len(result) == 1
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_filter_new_keys_empty_key_cols_with_prev_records(self):
        """Prev records exist but key_cols is empty (None) → all rows pass through (lines 75-76)."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j-ekc", name="j-ekc", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr",
                                 target_key_cols=None, source_key_cols=None)
            session.add(job)
            prev_run = TranslationRun(id="prev-run-ekc", job_id="j-ekc", status="COMPLETED",
                                      insert_status="succeeded",
                                      started_at=datetime.now(UTC),
                                      created_at=datetime.now(UTC))
            session.add(prev_run)
            session.flush()
            # Add prev records so the code reaches the key_cols check
            batch = TranslationBatch(id="batch-ekc", run_id="prev-run-ekc", batch_index=0,
                                     status="COMPLETED", created_at=datetime.now(UTC))
            session.add(batch)
            session.flush()
            rec = TranslationRecord(id="rec-ekc", run_id="prev-run-ekc", batch_id="batch-ekc",
                                    status="SUCCESS", source_data={"id": 1})
            session.add(rec)
            session.commit()

            svc = self._make_service(session)
            source_rows = [{"source_data": {"id": 1}}, {"source_data": {"id": 2}}]
            result = svc._filter_new_keys(job, "curr-run-ekc", source_rows)
            # key_cols = None or None or [] = [] → not key_cols → True → return source_rows
            assert len(result) == 2
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    # -- _load_preview_edits --

    def test_load_preview_edits_no_session(self):
        """No applied preview session -> empty cache."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j5", name="j5", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr")
            session.add(job)
            session.commit()
            svc = self._make_service(session)
            svc._load_preview_edits("j5")
            assert svc._preview_edits_cache == {}
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_load_preview_edits_skip_null_source_data(self):
        """Preview record with source_data=None is skipped (line 120)."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j-null-sd", name="j-null-sd", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr")
            session.add(job)
            ps = TranslationPreviewSession(id="ps-null", job_id="j-null-sd", status="APPLIED",
                                           created_at=datetime.now(UTC))
            session.add(ps)
            from src.models.translate import TranslationPreviewRecord as TPR
            rec = TPR(id="prec-null", session_id="ps-null", status="APPROVED",
                      source_data=None)
            session.add(rec)
            session.commit()

            svc = self._make_service(session)
            svc._load_preview_edits("j-null-sd")
            assert svc._preview_edits_cache == {}
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_load_preview_edits_with_edits(self):
        """Load preview edits from applied session."""
        session, engine = make_session()
        try:
            job = TranslationJob(id="j6", name="j6", status="ACTIVE",
                                 source_dialect="en", target_dialect="fr")
            session.add(job)
            ps = TranslationPreviewSession(id="ps-1", job_id="j6", status="APPLIED",
                                           created_at=datetime.now(UTC))
            session.add(ps)
            # The _load_preview_edits iterates rec.languages which is a Python attribute
            # on the model instance set after DB fetch, not a column.
            # We'll create a record with source_data, then patch the query to return
            # records with languages attached.
            from src.models.translate import TranslationPreviewRecord as TPR
            rec = TPR(id="prec-1", session_id="ps-1", status="APPROVED",
                      source_data={"id": 1})
            session.add(rec)
            session.commit()

            svc = self._make_service(session)

            # We need to mock the query to return a record with .languages
            class MockLangEntry:
                def __init__(self, code, status, user_edit=None, final_value=None):
                    self.language_code = code
                    self.status = status
                    self.user_edit = user_edit
                    self.final_value = final_value

            mock_rec = MagicMock(spec=TPR)
            mock_rec.source_data = {"id": 1}
            mock_rec.languages = [
                MockLangEntry("fr", "edited", "bonjour", "bonjour"),
                MockLangEntry("de", "approved", "hallo"),
            ]

            with patch.object(session, 'query', return_value=MagicMock()) as mock_query:
                mock_filter = MagicMock()
                mock_filter.order_by.return_value.first.return_value = ps
                mock_query.return_value.filter.return_value = mock_filter

                # Mock the records query
                mock_filter2 = MagicMock()
                mock_filter2.all.return_value = [mock_rec]
                mock_query.return_value.filter.return_value = mock_filter

                # Make first call return ps, second call return records
                mock_query.return_value.filter.return_value.order_by.return_value.first.return_value = ps
                # For the records query
                mock_records_filter = MagicMock()
                mock_records_filter.all.return_value = [mock_rec]
                mock_query.return_value.filter.return_value = mock_records_filter

                svc._load_preview_edits("j6")

            assert svc._preview_edits_cache is not None
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    # -- _compute_key_hash --

    def test_compute_key_hash(self):
        """_compute_key_hash returns deterministic 16-char hex."""
        from src.plugins.translate._run_service import RunExecutionService
        h1 = RunExecutionService._compute_key_hash({"a": 1, "b": 2})
        h2 = RunExecutionService._compute_key_hash({"a": 1, "b": 2})
        h3 = RunExecutionService._compute_key_hash({"a": 1, "b": 3})
        assert h1 == h2
        assert h1 != h3
        assert len(h1) == 16

    # -- _update_language_stats_incremental --

    def test_update_language_stats_empty(self):
        """No records -> no stats update (no-op)."""
        session, engine = make_session()
        try:
            svc = self._make_service(session)
            stats_map = {"fr": TranslationRunLanguageStats(run_id="r1", language_code="fr")}
            # No records for run-1, should be a no-op
            svc._update_language_stats_incremental("r1", stats_map)
            # Just check it doesn't raise
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_update_language_stats_with_records(self):
        """Update stats from records."""
        session, engine = make_session()
        try:
            run = TranslationRun(id="stats-run", job_id="no-job", status="COMPLETED",
                                 started_at=datetime.now(UTC))
            session.add(run)
            session.flush()
            batch = TranslationBatch(id="stats-batch", run_id="stats-run", batch_index=0,
                                     status="COMPLETED", created_at=datetime.now(UTC))
            session.add(batch)
            session.flush()
            rec = TranslationRecord(id="stats-rec", run_id="stats-run", batch_id="stats-batch",
                                    status="SUCCESS")
            session.add(rec)
            session.flush()

            lang = TranslationLanguage(id="l1", record_id="stats-rec",
                                       language_code="fr", status="translated",
                                       translated_value="bonjour")
            session.add(lang)
            session.commit()

            svc = self._make_service(session)
            lang_stat = TranslationRunLanguageStats(run_id="stats-run", language_code="fr",
                                                    total_rows=0, translated_rows=0)
            stats_map = {"fr": lang_stat}
            svc._update_language_stats_incremental("stats-run", stats_map)

            assert lang_stat.total_rows == 1
            assert lang_stat.translated_rows == 1
            assert lang_stat.failed_rows == 0
            assert lang_stat.skipped_rows == 0
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)

    def test_update_language_stats_multiple_statuses(self):
        """Multiple language entries with different statuses."""
        session, engine = make_session()
        try:
            run = TranslationRun(id="stats-run-2", job_id="no-job", status="COMPLETED",
                                 started_at=datetime.now(UTC))
            session.add(run)
            session.flush()
            batch = TranslationBatch(id="stats-batch-2", run_id="stats-run-2", batch_index=0,
                                     status="COMPLETED", created_at=datetime.now(UTC))
            session.add(batch)
            session.flush()
            # One record per unique (record_id, language_code) to avoid UNIQUE constraint
            rec_fr_translated = TranslationRecord(id="stats-rec-fr-t", run_id="stats-run-2",
                                                  batch_id="stats-batch-2", status="SUCCESS")
            rec_fr_failed = TranslationRecord(id="stats-rec-fr-f", run_id="stats-run-2",
                                              batch_id="stats-batch-2", status="SUCCESS")
            rec_de_skipped = TranslationRecord(id="stats-rec-de-s", run_id="stats-run-2",
                                               batch_id="stats-batch-2", status="SUCCESS")
            rec_de_translated = TranslationRecord(id="stats-rec-de-t", run_id="stats-run-2",
                                                  batch_id="stats-batch-2", status="SUCCESS")
            session.add_all([rec_fr_translated, rec_fr_failed, rec_de_skipped, rec_de_translated])
            session.flush()

            entries_data = [
                ("stats-rec-fr-t", "fr", "translated", "x"),
                ("stats-rec-fr-f", "fr", "failed", None),
                ("stats-rec-de-s", "de", "skipped", None),
                ("stats-rec-de-t", "de", "translated", "x"),
            ]
            for rec_id, code, st, tv in entries_data:
                session.add(TranslationLanguage(
                    id=str(uuid.uuid4()), record_id=rec_id,
                    language_code=code, status=st,
                    translated_value=tv,
                ))
            session.commit()

            svc = self._make_service(session)
            fr_stat = TranslationRunLanguageStats(run_id="stats-run-2", language_code="fr")
            de_stat = TranslationRunLanguageStats(run_id="stats-run-2", language_code="de")
            stats_map = {"fr": fr_stat, "de": de_stat}
            svc._update_language_stats_incremental("stats-run-2", stats_map)

            assert fr_stat.total_rows == 2
            assert fr_stat.translated_rows == 1
            assert fr_stat.failed_rows == 1
            assert de_stat.total_rows == 2
            assert de_stat.translated_rows == 1
            assert de_stat.skipped_rows == 1
        finally:
            session.close()
            Base.metadata.drop_all(bind=engine)
