# #region Test.TranslateJobService [C:3] [TYPE Module] [SEMANTICS test, translate, service, job, crud]
# @BRIEF Verify TranslateJobService contracts — CRUD operations, validation, date source integration.
# @RELATION BINDS_TO -> [TranslateJobService]
# @TEST_EDGE: missing_job -> Raises ValueError on get/update/delete
# @TEST_EDGE: invalid_upsert_strategy -> Raises ValueError
# @TEST_EDGE: invalid_bcp47 -> Validates target_languages
# @TEST_EDGE: datasource_not_found -> Graceful fallback
# @TEST_EDGE: environment_not_found -> Raises ValueError
import pydantic
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import UTC, datetime
import uuid

from src.models.translate import (
    TranslationJob, TranslationJobDictionary, TerminologyDictionary,
)
from src.schemas.translate import TranslateJobCreate, TranslateJobUpdate
from src.plugins.translate.service import TranslateJobService

from .conftest import JOB_ID


def _create_term_dict(session, dict_id="dict-1"):
    """Create a TerminologyDictionary row to satisfy FK constraints."""
    d = TerminologyDictionary(
        id=dict_id, name=f"Dict {dict_id}",
    )
    session.add(d)
    session.commit()
    return d


class TestInit:
    """Verify initialization."""

    def test_init(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config, current_user="test-user")
        assert svc.db is db_session
        assert svc.config_manager is config
        assert svc.current_user == "test-user"


class TestListJobs:
    """Verify list_jobs."""

    def test_list_all(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        total, jobs = svc.list_jobs()
        assert total >= 1
        assert any(j.id == JOB_ID for j in jobs)

    def test_list_filtered(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        total, jobs = svc.list_jobs(status_filter="ACTIVE")
        assert total >= 1

    def test_list_filtered_no_match(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        total, jobs = svc.list_jobs(status_filter="DRAFT")
        assert total == 0

    def test_list_pagination(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        total, jobs = svc.list_jobs(page=1, page_size=1)
        assert total >= 1
        assert len(jobs) <= 1


class TestGetJob:
    """Verify get_job."""

    def test_get_existing(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        job = svc.get_job(JOB_ID)
        assert job.id == JOB_ID
        assert job.name == "Base Test Job"

    def test_get_missing(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        with pytest.raises(ValueError, match="not found"):
            svc.get_job("nonexistent")


class TestCreateJob:
    """Verify create_job."""

    @pytest.mark.asyncio
    async def test_create_basic(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config, current_user="test-user")
        payload = TranslateJobCreate(
            name="New Job",
            source_dialect="en",
            target_dialect="fr",
            upsert_strategy="MERGE",
        )
        job = await svc.create_job(payload)
        assert job.name == "New Job"
        assert job.status == "DRAFT"
        assert job.created_by == "test-user"

    @pytest.mark.asyncio
    async def test_create_with_missing_translation_column(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobCreate(
            name="Bad Job",
            source_dialect="en",
            target_dialect="fr",
            source_datasource_id="123",
            translation_column=None,
            upsert_strategy="MERGE",
        )
        with pytest.raises(ValueError, match="translation column is required"):
            await svc.create_job(payload)

    @pytest.mark.asyncio
    async def test_create_invalid_upsert_strategy(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobCreate(
            name="Bad Job",
            source_dialect="en",
            target_dialect="fr",
            upsert_strategy="INVALID",
        )
        with pytest.raises(ValueError, match="Invalid upsert_strategy"):
            await svc.create_job(payload)

    @pytest.mark.asyncio
    async def test_create_with_dictionary_ids(self, db_session):
        """Create job with dictionary IDs. Requires TerminologyDictionary rows for FK."""
        _create_term_dict(db_session, "dict-1")
        _create_term_dict(db_session, "dict-2")
        config = MagicMock()
        svc = TranslateJobService(db_session, config, current_user="test-user")
        payload = TranslateJobCreate(
            name="Dict Job",
            source_dialect="en",
            target_dialect="fr",
            upsert_strategy="MERGE",
            dictionary_ids=["dict-1", "dict-2"],
        )
        job = await svc.create_job(payload)
        assocs = db_session.query(TranslationJobDictionary).filter(
            TranslationJobDictionary.job_id == job.id
        ).all()
        assert len(assocs) == 2

    @pytest.mark.asyncio
    async def test_create_with_datasource_metadata(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobCreate(
            name="DS Job",
            source_dialect="mysql",
            target_dialect="fr",
            source_datasource_id="42",
            environment_id="env-1",
            translation_column="text",
            upsert_strategy="MERGE",
        )
        with patch('src.plugins.translate.service.fetch_datasource_metadata',
                   new=AsyncMock(return_value=(None, "postgresql"))):
            job = await svc.create_job(payload)
            assert job.database_dialect == "postgresql"

    @pytest.mark.asyncio
    async def test_create_with_datasource_metadata_failure(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobCreate(
            name="DS Job Fail",
            source_dialect="mysql",
            target_dialect="fr",
            source_datasource_id="42",
            environment_id="env-1",
            translation_column="text",
            upsert_strategy="MERGE",
        )
        with patch('src.plugins.translate.service.fetch_datasource_metadata',
                   new=AsyncMock(side_effect=ValueError("API error"))):
            job = await svc.create_job(payload)
            assert job.database_dialect == "mysql"

    @pytest.mark.asyncio
    async def test_create_with_target_languages(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobCreate(
            name="Multi Lang",
            source_dialect="en",
            target_dialect="fr",
            target_languages=["fr", "de", "es"],
            upsert_strategy="MERGE",
        )
        job = await svc.create_job(payload)
        assert job.target_languages == ["fr", "de", "es"]

    @pytest.mark.asyncio
    async def test_create_with_single_target_language_string(self, db_session):
        """target_languages as a single string is normalized to list by service."""
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        # Pydantic v2 coerces str to list[str] if default_factory=list
        payload = TranslateJobCreate(
            name="Single Lang",
            source_dialect="en",
            target_language="es",
            target_languages=["es"],
            target_dialect="es",
            upsert_strategy="MERGE",
        )
        job = await svc.create_job(payload)
        assert "es" in job.target_languages

    @pytest.mark.asyncio
    async def test_create_invalid_bcp47(self, db_session):
        """BCP47 validation rejects invalid language tags."""
        with pytest.raises((ValueError, pydantic.ValidationError)):
            TranslateJobCreate(
                name="Bad BCP47",
                source_dialect="en",
                target_dialect="fr",
                target_languages=["invalid_lang_code"],
                upsert_strategy="MERGE",
            )


class TestUpdateJob:
    """Verify update_job."""

    @pytest.mark.asyncio
    async def test_update_name(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(name="Updated Name")
        job = await svc.update_job(JOB_ID, payload)
        assert job.name == "Updated Name"

    @pytest.mark.asyncio
    async def test_update_target_language(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(target_language="de")
        job = await svc.update_job(JOB_ID, payload)
        assert "de" in job.target_languages

    @pytest.mark.asyncio
    async def test_update_target_languages(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(target_languages=["fr", "de"])
        job = await svc.update_job(JOB_ID, payload)
        assert job.target_languages == ["fr", "de"]

    @pytest.mark.asyncio
    async def test_update_invalid_bcp47(self, db_session):
        """BCP47 validation on update rejects invalid tags."""
        with pytest.raises((ValueError, pydantic.ValidationError)):
            TranslateJobUpdate(target_languages=["bad lang code!!"])

    @pytest.mark.asyncio
    async def test_update_nonexistent_job(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(name="Ghost")
        with pytest.raises(ValueError, match="not found"):
            await svc.update_job("nonexistent", payload)

    @pytest.mark.asyncio
    async def test_update_with_dictionary_ids(self, db_session):
        """Update job with dictionary IDs. Requires TerminologyDictionary rows."""
        _create_term_dict(db_session, "dict-1")
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(dictionary_ids=["dict-1"])
        job = await svc.update_job(JOB_ID, payload)
        assocs = db_session.query(TranslationJobDictionary).filter(
            TranslationJobDictionary.job_id == job.id
        ).all()
        assert len(assocs) == 1

    @pytest.mark.asyncio
    async def test_update_with_source_datasource(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(
            source_datasource_id="99",
            environment_id="env-1",
        )
        with patch('src.plugins.translate.service.fetch_datasource_metadata',
                   new=AsyncMock(return_value=(None, "postgresql"))):
            job = await svc.update_job(JOB_ID, payload)
            assert job.database_dialect == "postgresql"

    @pytest.mark.asyncio
    async def test_update_with_source_datasource_failure(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(
            source_datasource_id="99",
            environment_id="env-1",
        )
        with patch('src.plugins.translate.service.fetch_datasource_metadata',
                   new=AsyncMock(side_effect=ValueError("API error"))):
            job = await svc.update_job(JOB_ID, payload)
            assert job is not None

    @pytest.mark.asyncio
    async def test_update_single_target_language_string(self, db_session):
        """target_languages passed as list (not string)."""
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        payload = TranslateJobUpdate(target_languages=["fr"])
        job = await svc.update_job(JOB_ID, payload)
        assert job.target_languages == ["fr"]


class TestDeleteJob:
    """Verify delete_job."""

    def test_delete_existing(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        svc.delete_job(JOB_ID)
        with pytest.raises(ValueError, match="not found"):
            svc.get_job(JOB_ID)

    def test_delete_nonexistent(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        with pytest.raises(ValueError, match="not found"):
            svc.delete_job("nonexistent")


class TestDuplicateJob:
    """Verify duplicate_job."""

    def test_duplicate_default_name(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        dup = svc.duplicate_job(JOB_ID)
        assert dup.name == "Base Test Job (Copy)"
        assert dup.status == "DRAFT"
        assert dup.id != JOB_ID

    def test_duplicate_custom_name(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        dup = svc.duplicate_job(JOB_ID, new_name="Custom Copy")
        assert dup.name == "Custom Copy"

    def test_duplicate_nonexistent(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        with pytest.raises(ValueError, match="not found"):
            svc.duplicate_job("nonexistent")


class TestGetJobDictionaryIds:
    """Verify get_job_dictionary_ids."""

    def test_no_dictionaries(self, db_session):
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        ids = svc.get_job_dictionary_ids(JOB_ID)
        assert ids == []
        assert isinstance(ids, list)

    def test_with_dictionaries(self, db_session):
        """Dictionary associations require TerminologyDictionary rows for FK."""
        _create_term_dict(db_session, "dict-1")
        _create_term_dict(db_session, "dict-2")
        config = MagicMock()
        svc = TranslateJobService(db_session, config)
        db_session.add(TranslationJobDictionary(
            id=str(uuid.uuid4()), job_id=JOB_ID, dictionary_id="dict-1",
        ))
        db_session.add(TranslationJobDictionary(
            id=str(uuid.uuid4()), job_id=JOB_ID, dictionary_id="dict-2",
        ))
        db_session.commit()
        ids = svc.get_job_dictionary_ids(JOB_ID)
        assert set(ids) == {"dict-1", "dict-2"}


class TestFetchAvailableDatasources:
    """Verify fetch_available_datasources."""

    @pytest.mark.asyncio
    async def test_fetch_with_search(self, db_session):
        config = MagicMock()
        config.get_environment.return_value = MagicMock(id="env-1")

        svc = TranslateJobService(db_session, config)
        mock_client = AsyncMock()
        mock_client.get_datasets.return_value = (None, [
            {"id": 1, "table_name": "orders", "schema": "public",
             "database": {"id": 10, "database_name": "Main DB", "backend": "postgresql"},
             "description": "Order data"},
            {"id": 2, "table_name": "users", "schema": "public",
             "database": {"id": 11, "database_name": "Main DB", "backend": "postgresql"},
             "description": "User data"},
        ])

        # Patch at the source module since service does lazy import
        with patch('src.core.utils.client_registry.get_superset_client',
                   new=AsyncMock(return_value=mock_client)):
            result = await svc.fetch_available_datasources("env-1", search="order")
            assert len(result) == 1
            assert result[0]["table_name"] == "orders"

    @pytest.mark.asyncio
    async def test_fetch_no_search(self, db_session):
        config = MagicMock()
        config.get_environment.return_value = MagicMock(id="env-1")

        svc = TranslateJobService(db_session, config)
        mock_client = AsyncMock()
        mock_client.get_datasets.return_value = (None, [
            {"id": 1, "table_name": "orders", "schema": "public",
             "database": {"id": 10, "database_name": "Main DB", "backend": "postgresql"},
             "description": ""},
        ])

        with patch('src.core.utils.client_registry.get_superset_client',
                   new=AsyncMock(return_value=mock_client)):
            result = await svc.fetch_available_datasources("env-1")
            assert len(result) == 1

    @pytest.mark.asyncio
    async def test_fetch_env_not_found(self, db_session):
        config = MagicMock()
        config.get_environment.return_value = None

        svc = TranslateJobService(db_session, config)
        with pytest.raises(ValueError, match="not found"):
            await svc.fetch_available_datasources("bad-env")
# #endregion Test.TranslateJobService
