# #region Test.PreviewExecutor [C:3] [TYPE Module] [SEMANTICS test,preview,executor,llm]
# @BRIEF Tests for preview_executor.py — PreviewExecutor class.
# @RELATION BINDS_TO -> [PreviewExecutor]
# @TEST_EDGE: no_env_config -> ValueError
# @TEST_EDGE: fetch_failure -> ValueError
# @TEST_EDGE: no_provider_id -> ValueError
# @TEST_EDGE: unsupported_provider -> ValueError

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

import pytest
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch

from src.models.translate import TranslationJob
from src.plugins.translate.preview_executor import PreviewExecutor
from .conftest import JOB_ID


class TestFetchSampleRows:
    """PreviewExecutor.fetch_sample_rows — fetch from Superset."""

    def _make_job(self):
        job = MagicMock(spec=TranslationJob)
        job.source_datasource_id = "1"
        job.environment_id = "env-1"
        job.source_dialect = None
        return job

    @pytest.mark.asyncio
    async def test_success(self, db_session):
        """Successful fetch returns extracted rows."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = self._make_job()
        job.environment_id = None
        job.source_dialect = None

        env_config = MagicMock()
        env_config.id = "env-1"

        config_manager = MagicMock()
        config_manager.get_environments.return_value = [env_config]

        executor.config_manager = config_manager

        with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
            mock_client = MagicMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
            mock_client.build_dataset_preview_query_context = MagicMock(return_value={
                "queries": [{}],
                "form_data": {},
            })
            mock_client.network.request = AsyncMock()
            mock_client.network.request.return_value = {"result": []}

            with patch("src.plugins.translate.preview_executor._extract_data_rows",
                       return_value=[{"text": "hello"}]):
                rows = await executor.fetch_sample_rows(job)
                assert len(rows) == 1

    @pytest.mark.asyncio
    async def test_no_env_config_fallback(self, db_session):
        """Falls back to first env config when env_id not found."""
        env_config = MagicMock()
        env_config.id = "env-other"

        config_manager = MagicMock()
        config_manager.get_environments.return_value = [env_config]

        executor = PreviewExecutor(db_session, config_manager)
        job = self._make_job()
        job.environment_id = "env-nonexistent"
        job.source_dialect = None

        with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
            mock_client = MagicMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
            mock_client.build_dataset_preview_query_context = MagicMock(return_value={
                "queries": [{}],
                "form_data": {},
            })
            mock_client.network.request = AsyncMock()
            mock_client.network.request.return_value = {"result": []}

            with patch("src.plugins.translate.preview_executor._extract_data_rows",
                       return_value=[{"text": "hello"}]):
                rows = await executor.fetch_sample_rows(job)
                assert len(rows) == 1

    @pytest.mark.asyncio
    async def test_no_environments_raises(self, db_session):
        """No environments configured raises ValueError."""
        config_manager = MagicMock()
        config_manager.get_environments.return_value = []

        executor = PreviewExecutor(db_session, config_manager)
        job = self._make_job()

        with pytest.raises(ValueError, match="No Superset environments"):
            await executor.fetch_sample_rows(job)

    @pytest.mark.asyncio
    async def test_network_error_raises(self, db_session):
        """Network error raises ValueError."""
        env_config = MagicMock()
        env_config.id = "env-1"

        config_manager = MagicMock()
        config_manager.get_environments.return_value = [env_config]

        executor = PreviewExecutor(db_session, config_manager)
        job = self._make_job()

        with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
            mock_client = MagicMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
            mock_client.build_dataset_preview_query_context = MagicMock(return_value={"queries": [{}], "form_data": {}})
            mock_client.network.request = AsyncMock(side_effect=Exception("Network error"))

            with pytest.raises(ValueError, match="Failed to fetch sample data"):
                await executor.fetch_sample_rows(job)

    @pytest.mark.asyncio
    async def test_query_context_structure(self, db_session):
        """Verify query context modifications (row_limit, result_type, etc.)."""
        env_config = MagicMock()
        env_config.id = "env-1"

        config_manager = MagicMock()
        config_manager.get_environments.return_value = [env_config]

        executor = PreviewExecutor(db_session, config_manager)
        job = self._make_job()

        with patch("src.plugins.translate.preview_executor.get_superset_client") as mock_get:
            mock_client = MagicMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail = AsyncMock(return_value={"id": 1})
            mock_client.build_dataset_preview_query_context = MagicMock(return_value={
                "queries": [{"result_type": "full", "columns": ["col1"], "metrics": ["count"]}],
                "form_data": {"query_mode": "aggregate"},
            })
            mock_client.network.request = AsyncMock()
            mock_client.network.request.return_value = {"result": []}

            with patch("src.plugins.translate.preview_executor._extract_data_rows",
                       return_value=[{"text": "hello"}]):
                rows = await executor.fetch_sample_rows(job, sample_size=5)
                assert len(rows) == 1
                # Verify query context was modified
                call_kwargs = mock_client.network.request.call_args[1]
                posted_data = call_kwargs["data"]
                import json
                parsed = json.loads(posted_data)
                assert parsed["queries"][0]["row_limit"] == 5
                assert "result_type" not in parsed["queries"][0]
                assert "columns" not in parsed["queries"][0]
                assert parsed["queries"][0]["metrics"] == []
                assert parsed["result_type"] == "samples"
                assert "query_mode" not in parsed["form_data"]


class TestCallLLM:
    """PreviewExecutor.call_llm — Call LLM provider."""

    def _make_job(self):
        job = MagicMock(spec=TranslationJob)
        job.provider_id = "prov-1"
        job.disable_reasoning = False
        return job

    def _make_provider(self, base_url="https://api.openai.com", provider_type="openai",
                       default_model="gpt-4o-mini"):
        provider = MagicMock()
        provider.base_url = base_url
        provider.provider_type = provider_type
        provider.default_model = default_model
        return provider

    @pytest.mark.asyncio
    async def test_success(self, db_session):
        """Successful LLM call returns content."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = self._make_job()

        with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
            mock_instance = MagicMock()
            mock_svc.return_value = mock_instance
            mock_instance.get_provider.return_value = self._make_provider()
            mock_instance.get_decrypted_api_key.return_value = "sk-test"

            with patch("src.plugins.translate.preview_executor.call_openai_compatible",
                       AsyncMock(return_value=("translated text", "stop"))):
                result = await executor.call_llm(job, "translate this", 4096)
                assert result == "translated text"

    @pytest.mark.asyncio
    async def test_no_provider_id(self, db_session):
        """No provider_id raises ValueError."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = MagicMock(spec=TranslationJob)
        job.provider_id = None
        with pytest.raises(ValueError, match="no LLM provider"):
            await executor.call_llm(job, "text")

    @pytest.mark.asyncio
    async def test_provider_not_found(self, db_session):
        """Provider not found in DB raises ValueError."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = self._make_job()

        with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
            mock_instance = MagicMock()
            mock_svc.return_value = mock_instance
            mock_instance.get_provider.return_value = None

            with pytest.raises(ValueError, match="not found"):
                await executor.call_llm(job, "text")

    @pytest.mark.asyncio
    async def test_no_api_key(self, db_session):
        """No decrypted API key raises ValueError."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = self._make_job()

        with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
            mock_instance = MagicMock()
            mock_svc.return_value = mock_instance
            mock_instance.get_provider.return_value = self._make_provider()
            mock_instance.get_decrypted_api_key.return_value = None

            with pytest.raises(ValueError, match="Could not decrypt API key"):
                await executor.call_llm(job, "text")

    @pytest.mark.asyncio
    async def test_unsupported_provider_type(self, db_session):
        """Unsupported provider type raises ValueError."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = self._make_job()

        with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
            mock_instance = MagicMock()
            mock_svc.return_value = mock_instance
            mock_instance.get_provider.return_value = self._make_provider(
                provider_type="anthropic"
            )
            mock_instance.get_decrypted_api_key.return_value = "sk-test"

            with pytest.raises(ValueError, match="Unsupported provider type"):
                await executor.call_llm(job, "text")

    @pytest.mark.asyncio
    async def test_empty_content_retry(self, db_session):
        """Empty content triggers retry with doubled max_tokens."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = self._make_job()

        with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
            mock_instance = MagicMock()
            mock_svc.return_value = mock_instance
            mock_instance.get_provider.return_value = self._make_provider()
            mock_instance.get_decrypted_api_key.return_value = "sk-test"

            # First call raises ValueError("empty content"), second succeeds
            with patch("src.plugins.translate.preview_executor.call_openai_compatible",
                       AsyncMock(side_effect=[
                           ValueError("empty content"),
                           ("retried content", "stop"),
                       ])):
                result = await executor.call_llm(job, "translate this", 4096)
                assert result == "retried content"

    @pytest.mark.asyncio
    async def test_empty_content_retry_exhausted(self, db_session):
        """If retry also returns empty content, raises."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = self._make_job()

        with patch("src.services.llm_provider.LLMProviderService") as mock_svc:
            mock_instance = MagicMock()
            mock_svc.return_value = mock_instance
            mock_instance.get_provider.return_value = self._make_provider()
            mock_instance.get_decrypted_api_key.return_value = "sk-test"

            with patch("src.plugins.translate.preview_executor.call_openai_compatible",
                       AsyncMock(side_effect=[ValueError("empty content"), ValueError("another error")])):
                with pytest.raises(ValueError, match="another error"):
                    await executor.call_llm(job, "text")


class TestDelegationHelpers:
    """PreviewExecutor delegation to preview_response_parser."""

    def test_parse_llm_response(self, db_session):
        """Delegates to module function."""
        executor = PreviewExecutor(db_session, MagicMock())
        with patch("src.plugins.translate.preview_executor._parse_llm_response") as mock:
            mock.return_value = {"0": {"fr": "bonjour"}}
            result = executor.parse_llm_response("text", 1, ["fr"])
            assert result == {"0": {"fr": "bonjour"}}

    def test_resolve_provider_model(self, db_session):
        """Delegates to module function."""
        executor = PreviewExecutor(db_session, MagicMock())
        job = MagicMock()
        with patch("src.plugins.translate.preview_executor._resolve_provider_model") as mock:
            mock.return_value = "gpt-4o"
            result = executor.resolve_provider_model(job)
            assert result == "gpt-4o"

    def test_compute_config_hash(self, db_session):
        """Delegates to module function."""
        with patch("src.plugins.translate.preview_executor._compute_config_hash") as mock:
            mock.return_value = "hash123"
            job = MagicMock()
            result = PreviewExecutor.compute_config_hash(job)
            assert result == "hash123"

    def test_compute_dict_snapshot_hash(self, db_session):
        """Delegates to module function."""
        executor = PreviewExecutor(db_session, MagicMock())
        with patch("src.plugins.translate.preview_executor._compute_dict_snapshot_hash") as mock:
            mock.return_value = "dict-hash"
            result = executor.compute_dict_snapshot_hash("job-1")
            assert result == "dict-hash"
# #endregion Test.PreviewExecutor
