# #region TranslateServiceIntegrationTests [C:4] [TYPE Module] [SEMANTICS test, integration, translate, service, postgres, testcontainers]
# @BRIEF Integration tests for TranslateJobService with real PostgreSQL via Testcontainers.
# @RELATION BINDS_TO -> [TranslateJobService]
# @RELATION BINDS_TO -> [TranslationJob]
# @RELATION BINDS_TO -> [TranslationJobDictionary]
# @TEST_CONTRACT TranslateJobService ->
# {
#   invariants: [
#     "create_job persists job with all fields",
#     "update_job modifies only specified fields",
#     "delete_job cascades to dictionary associations",
#     "duplicate_job copies all config with new ID",
#     "list_jobs respects pagination and status filter",
#     "JSON columns (source_key_cols, target_languages) round-trip correctly"
#   ]
# }
# @TEST_EDGE missing_translation_column -> ValueError when datasource set but no column
# @TEST_EDGE invalid_upsert_strategy -> ValueError on unsupported strategy
# @TEST_EDGE invalid_language_code -> ValueError on non-BCP47 language
# @TEST_EDGE job_not_found -> ValueError on get/update/delete of non-existent job
# @TEST_EDGE cascade_delete -> dictionary associations removed when job deleted
# @TEST_INVARIANT json_roundtrip -> VERIFIED_BY: [test_create_job_json_columns_persist]
# @TEST_INVARIANT cascade_integrity -> VERIFIED_BY: [test_delete_job_cascades_dictionary_associations]

import sys
from pathlib import Path

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

import pytest
from sqlalchemy.orm import Session
from unittest.mock import MagicMock, AsyncMock, patch

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


# #region TestTranslateJobServiceCRUD [C:3] [TYPE Class]
# @BRIEF Integration tests for job CRUD operations with real PostgreSQL.
class TestTranslateJobServiceCRUD:
    """Verify TranslateJobService CRUD with real PostgreSQL."""

    # region test_create_job_persists_all_fields [C:2] [TYPE Function]
    # @BRIEF Verify job creation persists all fields including JSON columns.
    @pytest.mark.asyncio
    async def test_create_job_persists_all_fields(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        payload = TranslateJobCreate(
            name="Integration Test Job",
            description="Testing real PostgreSQL persistence",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            source_key_cols=["id", "tenant_id"],
            target_key_cols=["id", "tenant_id"],
            translation_column="name",
            context_columns=["description", "category"],
            target_languages=["ru", "en", "de"],
            batch_size=100,
            upsert_strategy="MERGE",
        )

        job = await service.create_job(payload)

        assert job.id is not None
        assert job.name == "Integration Test Job"
        assert job.source_dialect == "postgresql"
        assert job.target_dialect == "clickhouse"
        assert job.status == "DRAFT"
        assert job.created_by == "test_user"

        # Verify JSON columns round-trip correctly
        assert job.source_key_cols == ["id", "tenant_id"]
        assert job.target_key_cols == ["id", "tenant_id"]
        assert job.context_columns == ["description", "category"]
        assert job.target_languages == ["ru", "en", "de"]
        assert job.batch_size == 100
        assert job.upsert_strategy == "MERGE"

        # Verify persisted in DB
        fetched = db_session.query(TranslationJob).filter(TranslationJob.id == job.id).first()
        assert fetched is not None
        assert fetched.name == "Integration Test Job"
        assert fetched.target_languages == ["ru", "en", "de"]
    # endregion test_create_job_persists_all_fields

    # region test_create_job_with_dictionary_associations [C:2] [TYPE Function]
    # @BRIEF Verify job creation with dictionary IDs creates associations.
    @pytest.mark.asyncio
    async def test_create_job_with_dictionary_associations(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        # Create dictionaries first
        dict1 = TerminologyDictionary(name="Dict 1", created_by="test_user")
        dict2 = TerminologyDictionary(name="Dict 2", created_by="test_user")
        db_session.add_all([dict1, dict2])
        db_session.commit()

        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        payload = TranslateJobCreate(
            name="Job With Dicts",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            batch_size=50,
            upsert_strategy="MERGE",
            dictionary_ids=[dict1.id, dict2.id],
        )

        job = await service.create_job(payload)

        # Verify associations
        associations = (
            db_session.query(TranslationJobDictionary)
            .filter(TranslationJobDictionary.job_id == job.id)
            .all()
        )
        assert len(associations) == 2
        dict_ids = {a.dictionary_id for a in associations}
        assert dict_ids == {dict1.id, dict2.id}
    # endregion test_create_job_with_dictionary_associations

    # region test_create_job_missing_translation_column_raises [C:2] [TYPE Function]
    # @BRIEF Verify ValueError when datasource set but no translation column.
    @pytest.mark.asyncio
    async def test_create_job_missing_translation_column_raises(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        payload = TranslateJobCreate(
            name="Bad Job",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            source_datasource_id="42",  # Datasource set
            # translation_column missing!
            batch_size=50,
            upsert_strategy="MERGE",
        )

        with pytest.raises(ValueError, match="translation column is required"):
            await service.create_job(payload)
    # endregion test_create_job_missing_translation_column_raises

    # region test_create_job_invalid_upsert_strategy_raises [C:2] [TYPE Function]
    # @BRIEF Verify ValueError on unsupported upsert strategy.
    @pytest.mark.asyncio
    async def test_create_job_invalid_upsert_strategy_raises(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        payload = TranslateJobCreate(
            name="Bad Strategy",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            batch_size=50,
            upsert_strategy="INVALID_STRATEGY",
        )

        with pytest.raises(ValueError, match="Invalid upsert_strategy"):
            await service.create_job(payload)
    # endregion test_create_job_invalid_upsert_strategy_raises

    # region test_create_job_invalid_language_raises [C:2] [TYPE Function]
    # @BRIEF Verify ValueError on non-BCP47 language code.
    @pytest.mark.asyncio
    async def test_create_job_invalid_language_raises(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        from pydantic import ValidationError

        # Pydantic validates at schema level, so we catch ValidationError
        with pytest.raises(ValidationError, match="target_languages"):
            TranslateJobCreate(
                name="Bad Language",
                source_dialect="postgresql",
                target_dialect="clickhouse",
                translation_column="name",
                batch_size=50,
                upsert_strategy="MERGE",
                target_languages=["123-invalid"],  # Starts with digits, invalid BCP47
            )
    # endregion test_create_job_invalid_language_raises

    # region test_get_job_not_found_raises [C:2] [TYPE Function]
    # @BRIEF Verify ValueError when job not found.
    def test_get_job_not_found_raises(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        with pytest.raises(ValueError, match="not found"):
            service.get_job("non-existent-job-id")
    # endregion test_get_job_not_found_raises

    # region test_update_job_modifies_specified_fields [C:2] [TYPE Function]
    # @BRIEF Verify update only modifies specified fields.
    @pytest.mark.asyncio
    async def test_update_job_modifies_specified_fields(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        # Create initial job
        create_payload = TranslateJobCreate(
            name="Original Name",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            batch_size=50,
            upsert_strategy="MERGE",
            target_languages=["ru"],
        )
        job = await service.create_job(create_payload)
        original_id = job.id
        original_batch_size = job.batch_size

        # Update only name and batch_size
        update_payload = TranslateJobUpdate(
            name="Updated Name",
            batch_size=200,
        )
        updated_job = await service.update_job(original_id, update_payload)

        assert updated_job.id == original_id
        assert updated_job.name == "Updated Name"
        assert updated_job.batch_size == 200
        # Unchanged fields
        assert updated_job.source_dialect == "postgresql"
        assert updated_job.target_dialect == "clickhouse"
        assert updated_job.target_languages == ["ru"]
    # endregion test_update_job_modifies_specified_fields

    # region test_update_job_dictionary_replacement [C:2] [TYPE Function]
    # @BRIEF Verify updating dictionary_ids replaces associations.
    @pytest.mark.asyncio
    async def test_update_job_dictionary_replacement(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        # Create dictionaries
        dict1 = TerminologyDictionary(name="Dict 1", created_by="test_user")
        dict2 = TerminologyDictionary(name="Dict 2", created_by="test_user")
        dict3 = TerminologyDictionary(name="Dict 3", created_by="test_user")
        db_session.add_all([dict1, dict2, dict3])
        db_session.commit()

        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        # Create job with dict1
        create_payload = TranslateJobCreate(
            name="Job",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            batch_size=50,
            upsert_strategy="MERGE",
            dictionary_ids=[dict1.id],
        )
        job = await service.create_job(create_payload)

        # Update to dict2 and dict3 (replacing dict1)
        update_payload = TranslateJobUpdate(dictionary_ids=[dict2.id, dict3.id])
        await service.update_job(job.id, update_payload)

        # Verify associations replaced
        dict_ids = service.get_job_dictionary_ids(job.id)
        assert set(dict_ids) == {dict2.id, dict3.id}
        assert dict1.id not in dict_ids
    # endregion test_update_job_dictionary_replacement

    # region test_delete_job_cascades_dictionary_associations [C:2] [TYPE Function]
    # @BRIEF Verify deleting job removes dictionary associations.
    @pytest.mark.asyncio
    async def test_delete_job_cascades_dictionary_associations(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        # Create dictionary and job with association
        dictionary = TerminologyDictionary(name="Test Dict", created_by="test_user")
        db_session.add(dictionary)
        db_session.commit()

        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        create_payload = TranslateJobCreate(
            name="To Delete",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            batch_size=50,
            upsert_strategy="MERGE",
            dictionary_ids=[dictionary.id],
        )
        job = await service.create_job(create_payload)
        job_id = job.id

        # Verify association exists
        assert len(service.get_job_dictionary_ids(job_id)) == 1

        # Delete job
        service.delete_job(job_id)

        # Verify job deleted
        with pytest.raises(ValueError, match="not found"):
            service.get_job(job_id)

        # Verify associations removed
        remaining = (
            db_session.query(TranslationJobDictionary)
            .filter(TranslationJobDictionary.job_id == job_id)
            .all()
        )
        assert len(remaining) == 0
    # endregion test_delete_job_cascades_dictionary_associations

    # region test_duplicate_job_copies_all_config [C:2] [TYPE Function]
    # @BRIEF Verify duplicate copies all config with new ID and DRAFT status.
    @pytest.mark.asyncio
    async def test_duplicate_job_copies_all_config(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        # Create dictionary
        dictionary = TerminologyDictionary(name="Test Dict", created_by="test_user")
        db_session.add(dictionary)
        db_session.commit()

        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        create_payload = TranslateJobCreate(
            name="Original Job",
            description="Original description",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            source_key_cols=["id"],
            target_key_cols=["id"],
            translation_column="name",
            context_columns=["category"],
            target_languages=["ru", "en"],
            batch_size=100,
            upsert_strategy="MERGE",
            dictionary_ids=[dictionary.id],
        )
        original = await service.create_job(create_payload)

        # Duplicate
        duplicate = service.duplicate_job(original.id, "Duplicated Job")

        # Verify new ID and name
        assert duplicate.id != original.id
        assert duplicate.name == "Duplicated Job"
        assert duplicate.status == "DRAFT"

        # Verify all config copied
        assert duplicate.description == original.description
        assert duplicate.source_dialect == original.source_dialect
        assert duplicate.target_dialect == original.target_dialect
        assert duplicate.source_key_cols == original.source_key_cols
        assert duplicate.target_key_cols == original.target_key_cols
        assert duplicate.translation_column == original.translation_column
        assert duplicate.context_columns == original.context_columns
        assert duplicate.target_languages == original.target_languages
        assert duplicate.batch_size == original.batch_size
        assert duplicate.upsert_strategy == original.upsert_strategy

        # Verify dictionary associations copied
        original_dict_ids = set(service.get_job_dictionary_ids(original.id))
        duplicate_dict_ids = set(service.get_job_dictionary_ids(duplicate.id))
        assert original_dict_ids == duplicate_dict_ids
    # endregion test_duplicate_job_copies_all_config

    # region test_list_jobs_pagination [C:2] [TYPE Function]
    # @BRIEF Verify list_jobs respects pagination.
    @pytest.mark.asyncio
    async def test_list_jobs_pagination(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        # Create 5 jobs
        for i in range(5):
            payload = TranslateJobCreate(
                name=f"Job {i}",
                source_dialect="postgresql",
                target_dialect="clickhouse",
                translation_column="name",
                batch_size=50,
                upsert_strategy="MERGE",
            )
            await service.create_job(payload)

        # Test pagination
        total, page1 = service.list_jobs(page=1, page_size=2)
        assert total == 5
        assert len(page1) == 2

        total, page2 = service.list_jobs(page=2, page_size=2)
        assert total == 5
        assert len(page2) == 2

        total, page3 = service.list_jobs(page=3, page_size=2)
        assert total == 5
        assert len(page3) == 1
    # endregion test_list_jobs_pagination

    # region test_list_jobs_status_filter [C:2] [TYPE Function]
    # @BRIEF Verify list_jobs filters by status.
    @pytest.mark.asyncio
    async def test_list_jobs_status_filter(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        # Create jobs with different statuses
        for i in range(3):
            payload = TranslateJobCreate(
                name=f"Draft Job {i}",
                source_dialect="postgresql",
                target_dialect="clickhouse",
                translation_column="name",
                batch_size=50,
                upsert_strategy="MERGE",
            )
            job = await service.create_job(payload)
            # Jobs are created as DRAFT

        # Create a READY job
        ready_payload = TranslateJobCreate(
            name="Ready Job",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            batch_size=50,
            upsert_strategy="MERGE",
        )
        ready_job = await service.create_job(ready_payload)
        ready_job.status = "READY"
        db_session.commit()

        # Filter by DRAFT
        total, draft_jobs = service.list_jobs(status_filter="DRAFT")
        assert total == 3
        assert all(j.status == "DRAFT" for j in draft_jobs)

        # Filter by READY
        total, ready_jobs = service.list_jobs(status_filter="READY")
        assert total == 1
        assert ready_jobs[0].status == "READY"
    # endregion test_list_jobs_status_filter

# #endregion TestTranslateJobServiceCRUD


# #region TestTranslateJobServiceJSONColumns [C:3] [TYPE Class]
# @BRIEF Integration tests for JSON column behavior with real PostgreSQL.
class TestTranslateJobServiceJSONColumns:
    """Verify JSON column round-trip with PostgreSQL JSONB."""

    # region test_json_columns_empty_arrays [C:2] [TYPE Function]
    # @BRIEF Verify empty arrays persist correctly in JSON columns.
    @pytest.mark.asyncio
    async def test_json_columns_empty_arrays(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        payload = TranslateJobCreate(
            name="Empty Arrays Job",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            source_key_cols=[],
            target_key_cols=[],
            context_columns=[],
            batch_size=50,
            upsert_strategy="MERGE",
        )
        job = await service.create_job(payload)

        # Verify empty arrays persist
        assert job.source_key_cols == []
        assert job.target_key_cols == []
        assert job.context_columns == []

        # Re-fetch from DB
        fetched = service.get_job(job.id)
        assert fetched.source_key_cols == []
        assert fetched.target_key_cols == []
        assert fetched.context_columns == []
    # endregion test_json_columns_empty_arrays

    # region test_json_columns_nested_objects [C:2] [TYPE Function]
    # @BRIEF Verify complex JSON structures persist correctly.
    @pytest.mark.asyncio
    async def test_json_columns_nested_objects(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        payload = TranslateJobCreate(
            name="Complex JSON Job",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            source_key_cols=["id", "tenant_id", "region"],
            target_key_cols=["id", "tenant_id", "region"],
            context_columns=["description", "category", "metadata.tags"],
            target_languages=["ru", "en", "de", "fr", "es"],
            batch_size=50,
            upsert_strategy="MERGE",
        )
        job = await service.create_job(payload)

        # Verify complex arrays persist
        assert job.source_key_cols == ["id", "tenant_id", "region"]
        assert job.target_key_cols == ["id", "tenant_id", "region"]
        assert job.context_columns == ["description", "category", "metadata.tags"]
        assert job.target_languages == ["ru", "en", "de", "fr", "es"]

        # Re-fetch and verify
        fetched = service.get_job(job.id)
        assert fetched.source_key_cols == ["id", "tenant_id", "region"]
        assert fetched.target_languages == ["ru", "en", "de", "fr", "es"]
    # endregion test_json_columns_nested_objects

    # region test_json_columns_null_handling [C:2] [TYPE Function]
    # @BRIEF Verify NULL JSON columns handled correctly.
    @pytest.mark.asyncio
    async def test_json_columns_null_handling(
        self, db_session: Session, mock_config_manager: MagicMock
    ):
        service = TranslateJobService(db_session, mock_config_manager, "test_user")

        payload = TranslateJobCreate(
            name="Null JSON Job",
            source_dialect="postgresql",
            target_dialect="clickhouse",
            translation_column="name",
            batch_size=50,
            upsert_strategy="MERGE",
            # No source_key_cols, target_key_cols, context_columns, target_languages
        )
        job = await service.create_job(payload)

        # Verify NULL handling
        assert job.source_key_cols is None or job.source_key_cols == []
        assert job.target_key_cols is None or job.target_key_cols == []
        assert job.context_columns is None or job.context_columns == []
        assert job.target_languages is None or job.target_languages == []
    # endregion test_json_columns_null_handling

# #endregion TestTranslateJobServiceJSONColumns

# #endregion TranslateServiceIntegrationTests
