#!/usr/bin/env python3
"""LLMPlotVerifier ドメインサービスのテスト

モックを使用した正常系・異常系・フォールバックテスト。
"""

import json
from unittest.mock import AsyncMock, Mock

import pytest

from noveler.application.validators.plot_adherence_validator import AdherenceElementType, PlotElement
from noveler.domain.services.llm_plot_verifier import LLMPlotVerifier, PlotVerificationError


@pytest.fixture
def mock_session_executor():
    """ClaudeSessionExecutorInterfaceのモック"""
    mock = Mock()
    mock.is_available = Mock(return_value=True)
    mock.execute_prompt = Mock(return_value={"success": True, "data": {}})
    return mock


@pytest.fixture
def mock_environment_detector():
    """EnvironmentDetectorInterfaceのモック"""
    mock = Mock()
    mock.is_claude_code_environment = Mock(return_value=True)
    mock.get_current_project_name = Mock(return_value="test_project")
    return mock


@pytest.fixture
def llm_verifier(mock_session_executor, mock_environment_detector):
    """LLMPlotVerifierインスタンス"""
    return LLMPlotVerifier(
        session_executor=mock_session_executor,
        environment_detector=mock_environment_detector,
    )


@pytest.fixture
def sample_plot_element():
    """サンプルプロット要素"""
    return PlotElement(
        element_type=AdherenceElementType.KEY_EVENT,
        description="主人公がギルドに到着する",
        required=True,
    )


@pytest.fixture
def sample_manuscript():
    """サンプル原稿"""
    return "主人公の太郎は冒険者ギルドの扉を押し開けた。受付には笑顔の女性がいた。"


class TestLLMPlotVerifierNormal:
    """正常系テスト"""

    @pytest.mark.asyncio
    async def test_verify_element_implemented_success(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """正常系: 完全実装の検証成功"""
        # LLMレスポンスをモック
        llm_response = {
            "success": True,
            "data": json.dumps({
                "status": "IMPLEMENTED",
                "evidence": "主人公の太郎は冒険者ギルドの扉を押し開けた。",
                "confidence": 0.95,
                "reason": "ギルド到着シーンが明確に描写されている",
            }),
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        result = await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

        assert result.status == "IMPLEMENTED"
        assert result.confidence == 0.95
        assert result.is_implemented()
        assert result.has_high_confidence()
        assert "ギルド" in result.evidence

    @pytest.mark.asyncio
    async def test_verify_element_partial_success(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """正常系: 部分実装の検証成功"""
        llm_response = {
            "success": True,
            "data": {
                "status": "PARTIAL",
                "evidence": "扉を押し開けた",
                "confidence": 0.6,
                "reason": "到着は示唆されているが明示的ではない",
            },
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        result = await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

        assert result.status == "PARTIAL"
        assert result.confidence == 0.6
        assert result.is_partial()
        assert not result.has_high_confidence()

    @pytest.mark.asyncio
    async def test_verify_element_missing_success(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """正常系: 未実装の検証成功"""
        llm_response = {
            "success": True,
            "data": {
                "status": "MISSING",
                "evidence": None,
                "confidence": 0.9,
                "reason": "ギルド到着に関する描写が見当たらない",
            },
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        result = await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

        assert result.status == "MISSING"
        assert result.is_missing()
        assert not result.has_evidence()

    @pytest.mark.asyncio
    async def test_verify_elements_batch_success(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
    ):
        """正常系: 複数要素の一括検証"""
        elements = [
            PlotElement(
                element_type=AdherenceElementType.KEY_EVENT,
                description="ギルド到着",
                required=True,
            ),
            PlotElement(
                element_type=AdherenceElementType.CHARACTER_DEVELOPMENT,
                description="主人公の性格描写",
                required=True,
            ),
        ]

        llm_response = {
            "success": True,
            "data": {
                "status": "IMPLEMENTED",
                "evidence": "test",
                "confidence": 0.8,
                "reason": "test",
            },
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        results = await llm_verifier.verify_elements_batch(sample_manuscript, elements)

        assert len(results) == 2
        assert all(r.status == "IMPLEMENTED" for r in results)


class TestLLMPlotVerifierAbnormal:
    """異常系テスト"""

    @pytest.mark.asyncio
    async def test_verify_element_invalid_status_response(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """異常系: 無効なstatus値のレスポンスは例外を投げる"""
        llm_response = {
            "success": True,
            "data": {
                "status": "INVALID_STATUS",  # 無効な値
                "evidence": "test",
                "confidence": 0.8,
                "reason": "test",
            },
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        with pytest.raises(PlotVerificationError):
            await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

    @pytest.mark.asyncio
    async def test_verify_element_json_parse_error(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """異常系: JSONパースエラーは例外を投げる"""
        llm_response = {
            "success": True,
            "data": "invalid json { this is not valid",
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        with pytest.raises(PlotVerificationError, match="パース"):
            await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

    @pytest.mark.asyncio
    async def test_verify_element_missing_required_fields(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """異常系: 必須フィールド欠落"""
        llm_response = {
            "success": True,
            "data": {
                "status": "IMPLEMENTED",
                # confidence, reason が欠落
            },
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        result = await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

        # デフォルト値でフォールバックされる
        assert result.status == "IMPLEMENTED"
        assert result.confidence == 0.5  # デフォルト値
        assert result.reason == "LLM判定"  # デフォルト値


class TestLLMPlotVerifierErrors:
    """エラーハンドリングテスト"""

    @pytest.mark.asyncio
    async def test_llm_unavailable_raises_error(
        self,
        mock_session_executor,
        mock_environment_detector,
        sample_manuscript,
        sample_plot_element,
    ):
        """異常系: LLM利用不可時は例外を投げる"""
        mock_session_executor.is_available.return_value = False
        verifier = LLMPlotVerifier(mock_session_executor, mock_environment_detector)

        with pytest.raises(PlotVerificationError, match="LLMセッションが利用できません"):
            await verifier.verify_element(sample_manuscript, sample_plot_element)

    @pytest.mark.asyncio
    async def test_llm_execution_error_raises(
        self,
        mock_session_executor,
        mock_environment_detector,
        sample_manuscript,
        sample_plot_element,
    ):
        """異常系: LLM実行エラー時は例外を投げる"""
        mock_session_executor.execute_prompt.side_effect = RuntimeError("LLM接続エラー")
        verifier = LLMPlotVerifier(mock_session_executor, mock_environment_detector)

        with pytest.raises(PlotVerificationError, match="LLMプロンプト実行に失敗"):
            await verifier.verify_element(sample_manuscript, sample_plot_element)


class TestLLMPlotVerifierThresholds:
    """閾値テスト（境界値テスト）"""

    @pytest.mark.asyncio
    async def test_confidence_threshold_zero(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """閾値: confidence = 0.0"""
        llm_response = {
            "success": True,
            "data": {"status": "MISSING", "confidence": 0.0, "reason": "確信なし"},
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        result = await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

        assert result.confidence == 0.0
        assert result.has_high_confidence(threshold=0.0)  # 0.0 >= 0.0 は True（境界値含む）

    @pytest.mark.asyncio
    async def test_confidence_threshold_one(
        self,
        llm_verifier,
        mock_session_executor,
        sample_manuscript,
        sample_plot_element,
    ):
        """閾値: confidence = 1.0"""
        llm_response = {
            "success": True,
            "data": {"status": "IMPLEMENTED", "confidence": 1.0, "reason": "100%確実"},
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        result = await llm_verifier.verify_element(sample_manuscript, sample_plot_element)

        assert result.confidence == 1.0
        assert result.has_high_confidence(threshold=1.0)

    @pytest.mark.asyncio
    async def test_manuscript_length_threshold_short(
        self,
        llm_verifier,
        mock_session_executor,
        sample_plot_element,
    ):
        """閾値: 原稿が極端に短い場合"""
        short_manuscript = "短い"  # 2文字
        llm_response = {
            "success": True,
            "data": {"status": "MISSING", "confidence": 0.9, "reason": "原稿が短すぎる"},
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        result = await llm_verifier.verify_element(short_manuscript, sample_plot_element)

        assert result.is_missing()

    @pytest.mark.asyncio
    async def test_manuscript_length_threshold_long(
        self,
        llm_verifier,
        mock_session_executor,
        sample_plot_element,
    ):
        """閾値: 原稿が3000文字を超える場合（切り詰められる）"""
        long_manuscript = "あ" * 5000  # 5000文字
        llm_response = {
            "success": True,
            "data": {"status": "PARTIAL", "confidence": 0.5, "reason": "一部のみ検証"},
        }
        mock_session_executor.execute_prompt.return_value = llm_response

        # プロンプト生成時に3000文字に制限されることを確認
        result = await llm_verifier.verify_element(long_manuscript, sample_plot_element)

        # execute_promptが呼ばれたことを確認
        assert mock_session_executor.execute_prompt.called
        # プロンプトに含まれる原稿が3000文字以下に制限されている
        call_args = mock_session_executor.execute_prompt.call_args
        prompt = call_args[1]["prompt"]
        assert len(prompt) < 6000  # プロンプト全体も短縮されている


class TestLLMPlotVerifierWhiteBox:
    """ホワイトボックステスト（内部実装の検証）"""

    def test_build_verification_prompt_structure(
        self,
        llm_verifier,
        sample_manuscript,
        sample_plot_element,
    ):
        """ホワイトボックス: プロンプト生成の構造検証"""
        prompt = llm_verifier._build_verification_prompt(sample_manuscript, sample_plot_element)

        # プロンプトに必須要素が含まれていることを確認
        assert "【原稿】" in prompt
        assert sample_manuscript in prompt
        assert "【検証項目】" in prompt
        assert sample_plot_element.description in prompt
        assert "【判定基準】" in prompt
        assert "IMPLEMENTED" in prompt
        assert "PARTIAL" in prompt
        assert "MISSING" in prompt
        assert "【出力形式（JSON）】" in prompt

    def test_build_verification_prompt_element_type_translation(self, llm_verifier):
        """ホワイトボックス: 要素タイプの日本語変換"""
        test_cases = [
            (AdherenceElementType.KEY_EVENT, "重要イベント"),
            (AdherenceElementType.CHARACTER_DEVELOPMENT, "キャラクター描写"),
            (AdherenceElementType.WORLD_BUILDING, "世界観設定"),
            (AdherenceElementType.FORESHADOWING, "伏線設置"),
        ]

        for element_type, expected_jp in test_cases:
            element = PlotElement(
                element_type=element_type,
                description="テスト",
                required=True,
            )
            prompt = llm_verifier._build_verification_prompt("原稿", element)
            assert expected_jp in prompt

    def test_parse_llm_response_dict_data(self, llm_verifier, sample_plot_element):
        """ホワイトボックス: 辞書型データのパース"""
        response = {
            "success": True,
            "data": {
                "status": "IMPLEMENTED",
                "evidence": "test evidence",
                "confidence": 0.85,
                "reason": "test reason",
            },
        }

        result = llm_verifier._parse_llm_response(response, sample_plot_element)

        assert result.status == "IMPLEMENTED"
        assert result.confidence == 0.85
        assert result.reason == "test reason"
        assert result.evidence == "test evidence"

    def test_parse_llm_response_string_data(self, llm_verifier, sample_plot_element):
        """ホワイトボックス: JSON文字列データのパース"""
        response = {
            "success": True,
            "data": json.dumps({
                "status": "PARTIAL",
                "confidence": 0.7,
                "reason": "partial implementation",
            }),
        }

        result = llm_verifier._parse_llm_response(response, sample_plot_element)

        assert result.status == "PARTIAL"
        assert result.confidence == 0.7

    def test_parse_llm_response_default_values(self, llm_verifier, sample_plot_element):
        """ホワイトボックス: デフォルト値の適用"""
        response = {
            "success": True,
            "data": {
                "status": "MISSING",
                # confidence, reason が欠落
            },
        }

        result = llm_verifier._parse_llm_response(response, sample_plot_element)

        assert result.status == "MISSING"
        assert result.confidence == 0.5  # デフォルト値
        assert result.reason == "LLM判定"  # デフォルト値


@pytest.mark.spec("SPEC-PLOT-ADHERENCE-001")
class TestLLMPlotVerifierIntegration:
    """統合シナリオテスト"""

    @pytest.mark.asyncio
    async def test_scenario_full_verification_flow(
        self,
        mock_session_executor,
        mock_environment_detector,
    ):
        """シナリオ: 完全な検証フロー"""
        verifier = LLMPlotVerifier(mock_session_executor, mock_environment_detector)
        manuscript = "主人公は冒険者ギルドに到着し、受付で初めての依頼を受け取った。彼は少し緊張していた。"

        # 複数要素の検証
        elements = [
            PlotElement(
                element_type=AdherenceElementType.KEY_EVENT,
                description="ギルド到着",
                required=True,
            ),
            PlotElement(
                element_type=AdherenceElementType.CHARACTER_DEVELOPMENT,
                description="主人公の緊張",
                required=True,
            ),
            PlotElement(
                element_type=AdherenceElementType.WORLD_BUILDING,
                description="魔法学園の描写",
                required=True,
            ),
        ]

        # LLMレスポンスをモック（要素ごとに異なる結果）
        responses = [
            {"success": True, "data": {"status": "IMPLEMENTED", "confidence": 0.9, "reason": "明確に描写"}},
            {"success": True, "data": {"status": "PARTIAL", "confidence": 0.6, "reason": "間接的言及"}},
            {"success": True, "data": {"status": "MISSING", "confidence": 0.85, "reason": "記述なし"}},
        ]
        mock_session_executor.execute_prompt.side_effect = responses

        results = await verifier.verify_elements_batch(manuscript, elements)

        assert len(results) == 3
        assert results[0].is_implemented()
        assert results[1].is_partial()
        assert results[2].is_missing()
