#!/usr/bin/env python3
"""LLMImprovementSuggestionService ユニットテスト

LLMベース改善提案生成機能のテスト
SPEC-PLOT-ADHERENCE-001 Section 2.2.4 高度版実装
"""

from unittest.mock import AsyncMock, MagicMock

import pytest

from noveler.application.services.llm_improvement_suggestion_service import LLMImprovementSuggestionService
from noveler.application.validators.plot_adherence_validator import AdherenceElementType, PlotElement


@pytest.mark.spec("SPEC-PLOT-ADHERENCE-001")
class TestLLMImprovementSuggestionService:
    """LLMImprovementSuggestionService のテストクラス"""

    # ========================================
    # 正常系テスト
    # ========================================

    @pytest.mark.asyncio
    async def test_generate_suggestions_with_llm(self):
        """正常系: LLMから改善提案を生成"""
        # Arrange
        mock_executor = MagicMock()
        mock_executor.execute_prompt = MagicMock(
            return_value="""{
            "suggestions": [
                {
                    "element_type": "KEY_EVENT",
                    "description": "戦闘シーン",
                    "improvement_text": "123行目の戦闘シーンで主人公の心理描写を追加してください",
                    "impact": "キャラクター成長が明確になります",
                    "priority": "高"
                }
            ]
        }"""
        )

        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        manuscript = "主人公は城に向かった。敵と戦った。勝利した。"
        missing_elements = [
            PlotElement(
                element_type=AdherenceElementType.KEY_EVENT,
                description="戦闘シーン",
                required=True,
            )
        ]

        # Act
        suggestions = await service.generate_suggestions(
            manuscript=manuscript,
            missing_elements=missing_elements,
            extra_elements=[],
            adherence_score=75.0,
        )

        # Assert
        assert len(suggestions) > 0
        assert any("戦闘" in s for s in suggestions)
        mock_executor.execute_prompt.assert_called_once()

    @pytest.mark.asyncio
    async def test_generate_suggestions_no_missing_elements(self):
        """正常系: 不足要素がない場合"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        manuscript = "原稿テキスト"
        missing_elements: list[PlotElement] = []

        # Act
        suggestions = await service.generate_suggestions(
            manuscript=manuscript,
            missing_elements=missing_elements,
            extra_elements=[],
            adherence_score=95.0,
        )

        # Assert
        assert len(suggestions) > 0
        assert any("優秀" in s for s in suggestions)
        # LLMは呼ばれない
        mock_executor.execute_prompt.assert_not_called()

    @pytest.mark.asyncio
    async def test_generate_suggestions_fallback_on_llm_error(self):
        """正常系: LLMエラー時にテンプレート提案にフォールバック"""
        # Arrange
        mock_executor = MagicMock()
        mock_executor.execute_prompt = MagicMock(
            side_effect=RuntimeError("LLM接続エラー")
        )

        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        manuscript = "原稿テキスト"
        missing_elements = [
            PlotElement(
                element_type=AdherenceElementType.KEY_EVENT,
                description="重要な会議",
                required=True,
            )
        ]

        # Act
        suggestions = await service.generate_suggestions(
            manuscript=manuscript,
            missing_elements=missing_elements,
            extra_elements=[],
            adherence_score=75.0,
        )

        # Assert
        assert len(suggestions) > 0
        # テンプレート提案が使用されている
        assert any("重要イベント" in s for s in suggestions)

    # ========================================
    # 異常系テスト
    # ========================================

    def test_parse_llm_response_valid_json(self):
        """異常系: 有効なJSON応答をパース"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        response = """{
            "suggestions": [
                {
                    "element_type": "KEY_EVENT",
                    "description": "イベント",
                    "improvement_text": "イベント描写を追加",
                    "impact": "効果あり",
                    "priority": "高"
                }
            ]
        }"""

        # Act
        suggestions = service._parse_llm_response(response)

        # Assert
        assert len(suggestions) > 0
        assert "KEY_EVENT" in suggestions[0]
        assert "イベント描写を追加" in suggestions[0]

    def test_parse_llm_response_invalid_json(self):
        """異常系: 不正なJSON形式"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        response = "invalid json"

        # Act & Assert
        with pytest.raises(ValueError, match="パースに失敗"):
            service._parse_llm_response(response)

    def test_parse_llm_response_missing_suggestions_key(self):
        """異常系: suggestionsキーが欠けている"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        response = '{"other_key": []}'

        # Act & Assert
        with pytest.raises(ValueError, match="suggestionsキーがありません"):
            service._parse_llm_response(response)

    def test_parse_llm_response_suggestions_not_list(self):
        """異常系: suggestionsがリスト型ではない"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        response = '{"suggestions": "not_a_list"}'

        # Act & Assert
        with pytest.raises(ValueError, match="リストである必要があります"):
            service._parse_llm_response(response)

    # ========================================
    # 設定テスト
    # ========================================

    def test_set_temperature_valid(self):
        """設定: 有効な温度値"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act
        service.set_temperature(0.5)

        # Assert
        assert service._temperature == 0.5

    def test_set_temperature_invalid_high(self):
        """設定: 温度値が高すぎる"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act & Assert
        with pytest.raises(ValueError, match="0.0-1.0の範囲"):
            service.set_temperature(1.5)

    def test_set_temperature_invalid_low(self):
        """設定: 温度値が低すぎる"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act & Assert
        with pytest.raises(ValueError, match="0.0-1.0の範囲"):
            service.set_temperature(-0.1)

    def test_set_timeout_valid(self):
        """設定: 有効なタイムアウト値"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act
        service.set_timeout(60)

        # Assert
        assert service._timeout == 60

    def test_set_timeout_invalid_zero(self):
        """設定: タイムアウト値がゼロ"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act & Assert
        with pytest.raises(ValueError, match="正の数である必要があります"):
            service.set_timeout(0)

    def test_set_timeout_invalid_negative(self):
        """設定: タイムアウト値が負数"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act & Assert
        with pytest.raises(ValueError, match="正の数である必要があります"):
            service.set_timeout(-10)

    # ========================================
    # 境界値テスト
    # ========================================

    def test_get_generic_suggestions_excellent_score(self):
        """境界値: 優秀スコア（95%以上）"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act
        suggestions = service._get_generic_suggestions(adherence_score=95.0)

        # Assert
        assert len(suggestions) > 0
        assert any("優秀" in s for s in suggestions)

    def test_get_generic_suggestions_acceptable_score(self):
        """境界値: 許容スコア（80-95%）"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act
        suggestions = service._get_generic_suggestions(adherence_score=87.5)

        # Assert
        assert len(suggestions) > 0
        assert any("調整" in s for s in suggestions)

    def test_get_generic_suggestions_poor_score(self):
        """境界値: 低いスコア（80%未満）"""
        mock_executor = MagicMock()
        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        # Act
        suggestions = service._get_generic_suggestions(adherence_score=70.0)

        # Assert
        assert len(suggestions) > 0
        assert any("改善" in s for s in suggestions)

    @pytest.mark.asyncio
    async def test_generate_suggestions_multiple_missing_elements(self):
        """境界値: 複数の不足要素"""
        mock_executor = MagicMock()
        mock_executor.execute_prompt = MagicMock(
            return_value='{"suggestions": []}'
        )

        service = LLMImprovementSuggestionService(session_executor=mock_executor)

        manuscript = "原稿"
        missing_elements = [
            PlotElement(
                element_type=AdherenceElementType.KEY_EVENT,
                description="イベント1",
            ),
            PlotElement(
                element_type=AdherenceElementType.CHARACTER_DEVELOPMENT,
                description="キャラクター描写",
            ),
            PlotElement(
                element_type=AdherenceElementType.WORLD_BUILDING,
                description="世界観",
            ),
        ]

        # Act
        suggestions = await service.generate_suggestions(
            manuscript=manuscript,
            missing_elements=missing_elements,
            extra_elements=[],
            adherence_score=65.0,
        )

        # Assert
        assert len(suggestions) > 0
        # LLMが呼ばれている
        mock_executor.execute_prompt.assert_called_once()
