"""Regression tests for 3 bug fixes: flush, append_log, plan_id fallback."""

from __future__ import annotations

import json
import os
from unittest.mock import MagicMock, patch

import pytest

os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")


# ── Bug 1: db.flush() before pending query ─────────────────────────────────


class TestReplaceAllClipsFlush:
    """replace_all_clips_transactional must flush before querying pending clips."""

    def _make_svc_and_db(self, pending_results):
        """Helper: create service + db mock. pending_results = list returned by pending query."""
        from app.services.edit_plan_service import EditPlanService

        db = MagicMock()
        # Delete query
        delete_query = MagicMock()
        delete_query.filter.return_value.delete.return_value = 0
        # Pending query: single .filter() with multiple conditions
        ready_query = MagicMock()
        ready_query.filter.return_value.all.return_value = pending_results
        db.query.side_effect = [delete_query, ready_query]
        return db, EditPlanService

    def _setup_clip_mocks(self, mock_clip_cls, mock_model_cls, asset_id="asset-1"):
        mock_entity = MagicMock()
        mock_entity.id = "clip-1"
        mock_entity.plan_id = "plan-1"
        mock_entity.clip_type = "main"
        mock_entity.order = 0
        mock_entity.asset_id = asset_id
        mock_entity.text_content = ""
        mock_entity.start_time = 0.0
        mock_entity.duration = 3.0
        mock_entity.transition_effect = "cut"
        mock_entity.transition_duration = 0.0
        mock_entity.playback_speed = 1.0
        mock_entity.status.value = "pending"
        mock_entity.config = {}
        mock_clip_cls.create.return_value = mock_entity
        mock_model_cls.return_value = MagicMock()
        return mock_entity

    @patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
    @patch("app.services.edit_plan_service.EditPlanClip")
    def test_flush_called_between_add_and_query(self, mock_clip_cls, mock_model_cls):
        """db.flush() must be called after db.add() and before the pending query."""
        self._setup_clip_mocks(mock_clip_cls, mock_model_cls)
        db, SvcClass = self._make_svc_and_db([])

        clip_repo = MagicMock()
        clip_repo.session = db
        svc = SvcClass.__new__(SvcClass)
        svc._clip_repo = clip_repo

        svc.replace_all_clips_transactional(
            "plan-1",
            [{"asset_id": "asset-1", "start_time": 0.0, "duration": 3.0, "order": 0}],
        )

        db.flush.assert_called_once()
        # Verify ordering: add → flush → query → commit
        method_names = [c[0] for c in db.method_calls]
        add_idx = method_names.index("add")
        flush_idx = method_names.index("flush")
        commit_idx = method_names.index("commit")
        assert add_idx < flush_idx < commit_idx

    @patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
    @patch("app.services.edit_plan_service.EditPlanClip")
    def test_flush_marks_new_clips_ready(self, mock_clip_cls, mock_model_cls):
        """After flush, new clips with asset_id are found and marked ready."""
        self._setup_clip_mocks(mock_clip_cls, mock_model_cls)

        # Use a plain object so we can verify attribute mutation
        class FakeClip:
            status = "pending"

        pending_clip = FakeClip()
        db, SvcClass = self._make_svc_and_db([pending_clip])

        clip_repo = MagicMock()
        clip_repo.session = db
        svc = SvcClass.__new__(SvcClass)
        svc._clip_repo = clip_repo

        svc.replace_all_clips_transactional(
            "plan-1",
            [{"asset_id": "asset-1", "start_time": 0.0, "duration": 3.0, "order": 0}],
        )

        assert pending_clip.status == "ready"


# ── Bug 2: append_log no TypeError ─────────────────────────────────────────


class TestAppendLogNoConflict:
    """append_log must not receive duplicate 'stage' parameter."""

    def _make_task(self):
        from packages.domain.generation_task import GenerationTask

        return GenerationTask.create(
            project_id="proj-1",
            asset_library_id="lib-1",
            strategy_id="one_take",
            template_id="tpl-1",
            asset_ids=["a1"],
            created_by_user_id="user-1",
        )

    def test_append_log_with_stage_as_first_positional(self):
        """append_log(stage, message, ...) works correctly."""
        task = self._make_task()
        task.append_log("render", "some error", level="ERROR", error_type="RuntimeError")

        entries = json.loads(task.logs)
        assert len(entries) == 1
        assert entries[0]["stage"] == "render"
        assert entries[0]["message"] == "some error"
        assert entries[0]["level"] == "ERROR"
        assert entries[0]["error_type"] == "RuntimeError"

    def test_duplicate_stage_raises_type_error(self):
        """Sanity check: passing stage both positionally and as kwarg raises TypeError."""
        task = self._make_task()
        with pytest.raises(TypeError):
            task.append_log(
                "任务失败",  # positional → stage
                "some error",
                level="ERROR",
                stage="render",  # duplicate → TypeError
            )


# ── Bug 3: plan_id fallback in create_generation_task ──────────────────────


class TestPlanIdFallback:
    """Formal generation API should fallback to find plan by template_id + user_id."""

    def test_fallback_code_present_in_source(self):
        """Verify the fallback logic is present in the generation_tasks module."""
        import inspect

        from apps.api.app.api.routes import generation_tasks

        source = inspect.getsource(generation_tasks.create_generation_task)
        assert "EditPlanModel" in source
        assert "兜底关联编辑计划" in source
        assert "自动关联编辑计划" in source

    def test_fallback_only_runs_when_source_edit_plan_id_empty(self):
        """Verify the condition checks for empty source_edit_plan_id."""
        import inspect

        from apps.api.app.api.routes import generation_tasks

        source = inspect.getsource(generation_tasks.create_generation_task)
        assert "not task.source_edit_plan_id and request.template_id" in source

    def test_list_by_template_method_exists(self):
        """Verify SQLAlchemyEditPlanRepository.list_by_template is callable."""
        from packages.adapters.sqlalchemy_impl.edit_plan_repository import (
            SQLAlchemyEditPlanRepository,
        )

        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_db.query.return_value = mock_session
        mock_session.filter.return_value = mock_session
        mock_session.order_by.return_value = mock_session
        mock_session.offset.return_value = mock_session
        mock_session.limit.return_value.all.return_value = []

        repo = SQLAlchemyEditPlanRepository(mock_db)
        result = repo.list_by_template("tpl-1", limit=20)
        assert result == []


class TestPlanIdFallbackExecution:
    """Test that the fallback logic actually executes when source_edit_plan_id is empty."""

    @staticmethod
    def _make_mock_task(source_edit_plan_id=""):
        t = MagicMock()
        t.id = "task-1"
        t.project_id = "proj-1"
        t.asset_library_id = ""
        t.strategy_id = "one_take"
        t.voice_library_id = ""
        t.template_id = "tpl-1"
        t.asset_ids = []
        t.title_ids = []
        t.voice_ids = []
        t.source_edit_plan_id = source_edit_plan_id
        t.asset_select_mode = "manual"
        t.batch_id = ""
        t.video_title = ""
        t.resolution = ""
        t.bgm_config = None
        t.is_preview = False
        t.source_task_id = ""
        t.output_width = 1280
        t.output_height = 720
        t.cover_url = ""
        t.title_config = {}
        t.logs = "[]"
        t.status = "pending"
        t.progress = 0.0
        t.error_message = ""
        t.error_info = None
        t.created_at = "2026-01-01T00:00:00Z"
        t.updated_at = "2026-01-01T00:00:00Z"
        t.started_at = None
        t.completed_at = None
        t.created_by_user_id = "user-1"
        t.auto_retry_enabled = False
        t.auto_retry_max = 0
        t.auto_retry_count = 0
        t.result_count = 0
        return t

    @staticmethod
    def _make_request(source_edit_plan_id="", template_id="tpl-1"):
        req = MagicMock()
        req.template_id = template_id
        req.source_edit_plan_id = source_edit_plan_id
        req.asset_ids = []
        req.asset_select_mode = "manual"
        req.asset_select_count = 0
        req.voice_library_id = ""
        req.title_ids = []
        req.voice_ids = []
        req.strategy_id = "one_take"
        req.count = 1
        req.video_title = ""
        req.resolution = ""
        req.bgm_config = None
        req.auto_retry_enabled = False
        req.auto_retry_max = 0
        req.is_preview = False
        req.source_task_id = ""
        req.output_width = 0
        req.output_height = 0
        req.cover_url = ""
        req.title_config = {}
        req.project_id = None
        req.asset_library_id = None
        return req

    def _run_create_task(self, mock_task, mock_request, mock_db, mock_gen_repo):
        """Helper to run create_generation_task with common mocks."""
        from app.schemas.generation_task import GenerationTaskResponse

        with (
            patch(
                "apps.api.app.api.routes.generation_tasks._resolve_project_and_library", return_value=("proj-1", None)
            ),
            patch("apps.api.app.api.routes.generation_tasks.CreateGenerationTaskUseCase") as mock_uc_cls,
            patch("apps.api.app.api.routes.generation_tasks.safe_enqueue_generation_task", return_value=True),
            patch("apps.api.app.api.routes.generation_tasks._to_generation_task_response") as mock_resp_fn,
        ):
            mock_uc_cls.return_value.execute.return_value = mock_task
            mock_resp_fn.return_value = GenerationTaskResponse(
                id="task-1",
                project_id="proj-1",
                asset_library_id="",
                strategy_id="one_take",
                voice_library_id="",
                template_id="tpl-1",
                asset_ids=[],
                title_ids=[],
                voice_ids=[],
                source_edit_plan_id="",
                asset_select_mode="",
                batch_id="",
                video_title="",
                resolution="",
                bgm_config={},
                is_preview=False,
                source_task_id="",
                output_width=1280,
                output_height=720,
                cover_url="",
                title_config={},
                logs="[]",
                status="pending",
                progress=0.0,
                error_message="",
                created_at="2026-01-01T00:00:00Z",
                updated_at="2026-01-01T00:00:00Z",
                started_at=None,
                completed_at=None,
                created_by_user_id="user-1",
                auto_retry_enabled=False,
                auto_retry_max=0,
                auto_retry_count=0,
                result_count=0,
            )

            from apps.api.app.api.routes.generation_tasks import create_generation_task

            return create_generation_task(
                request=mock_request,
                authenticated_user=MagicMock(user=MagicMock(id="user-1")),
                generation_task_repository=mock_gen_repo,
                project_repository=MagicMock(),
                asset_library_repository=MagicMock(),
                asset_repository=MagicMock(),
                db=mock_db,
            )

    def test_fallback_sets_plan_id_when_empty(self):
        """When source_edit_plan_id is empty, fallback finds plan via DB query."""
        mock_task = self._make_mock_task(source_edit_plan_id="")
        mock_request = self._make_request(source_edit_plan_id="", template_id="tpl-1")

        # Mock DB query chain: db.query(EditPlanModel).filter(...).order_by(...).first()
        mock_plan_model = MagicMock()
        mock_plan_model.id = "plan-found-123"

        mock_db = MagicMock()
        query_chain = MagicMock()
        query_chain.filter.return_value = query_chain
        query_chain.order_by.return_value = query_chain
        query_chain.first.return_value = mock_plan_model
        mock_db.query.return_value = query_chain

        mock_gen_repo = MagicMock()
        mock_gen_repo.count_pending_by_user.return_value = 0
        mock_gen_repo.count_pending_total.return_value = 0

        self._run_create_task(mock_task, mock_request, mock_db, mock_gen_repo)

        assert mock_task.source_edit_plan_id == "plan-found-123"
        mock_gen_repo.update.assert_called_once_with(mock_task)

    def test_no_fallback_when_plan_id_already_set(self):
        """When source_edit_plan_id is already set, fallback should NOT run."""
        mock_task = self._make_mock_task(source_edit_plan_id="plan-already-set")
        mock_request = self._make_request(source_edit_plan_id="plan-already-set", template_id="tpl-1")

        mock_db = MagicMock()
        mock_gen_repo = MagicMock()
        mock_gen_repo.count_pending_by_user.return_value = 0
        mock_gen_repo.count_pending_total.return_value = 0

        self._run_create_task(mock_task, mock_request, mock_db, mock_gen_repo)

        assert mock_task.source_edit_plan_id == "plan-already-set"
        mock_gen_repo.update.assert_not_called()

    def test_fallback_no_match_leaves_plan_id_empty(self):
        """When no plan matches, source_edit_plan_id stays empty."""
        mock_task = self._make_mock_task(source_edit_plan_id="")
        mock_request = self._make_request(source_edit_plan_id="", template_id="tpl-1")

        mock_db = MagicMock()
        query_chain = MagicMock()
        query_chain.filter.return_value = query_chain
        query_chain.order_by.return_value = query_chain
        query_chain.first.return_value = None  # no matching plan
        mock_db.query.return_value = query_chain

        mock_gen_repo = MagicMock()
        mock_gen_repo.count_pending_by_user.return_value = 0
        mock_gen_repo.count_pending_total.return_value = 0

        self._run_create_task(mock_task, mock_request, mock_db, mock_gen_repo)

        assert mock_task.source_edit_plan_id == ""
        mock_gen_repo.update.assert_not_called()

    def test_fallback_handles_exception_gracefully(self):
        """When DB query fails, the fallback should not break the main flow."""
        mock_task = self._make_mock_task(source_edit_plan_id="")
        mock_request = self._make_request(source_edit_plan_id="", template_id="tpl-1")

        mock_db = MagicMock()
        mock_db.query.side_effect = Exception("DB connection error")

        mock_gen_repo = MagicMock()
        mock_gen_repo.count_pending_by_user.return_value = 0
        mock_gen_repo.count_pending_total.return_value = 0

        self._run_create_task(mock_task, mock_request, mock_db, mock_gen_repo)

        # Task should still be created (fallback error doesn't break main flow)
        assert mock_task.source_edit_plan_id == ""
        mock_gen_repo.update.assert_not_called()
