from __future__ import annotations

import unittest
from types import SimpleNamespace
from unittest.mock import patch

from backend.features.heaven.agent import (
    HeavenAgentError,
    _system_prompt,
    _validate_answer,
    interpret_heaven,
)


class HeavenPromptTests(unittest.TestCase):
    def test_trend_prompt_requires_direction_without_fixed_advice_vocabulary(self):
        prompt = _system_prompt("trend")

        self.assertIn("必须明确给出卦义上的当下倾向", prompt)
        self.assertIn("不得用“谨慎、等待、守信、辨伪”一类泛化劝诫代替解势", prompt)
        self.assertNotIn("卦理约占九成", prompt)
        self.assertNotIn("全文控制在300至450", prompt)
        self.assertIn("## 核心判断", prompt)
        self.assertIn("## 决策映射", prompt)

    def test_fortune_prompt_excludes_weighted_conclusion(self):
        prompt = _system_prompt("fortune")

        self.assertIn("五行权重、主导元素和预制复合断语已明确排除", prompt)
        self.assertIn("相生不直接等于吉，相克不直接等于凶", prompt)
        self.assertIn("不得使用藏干", prompt)
        self.assertIn("只能使用knowledge中本次命中的关系释义", prompt)
        self.assertNotIn("全文控制在420至600", prompt)
        self.assertIn("calculation.industry_symbols", prompt)
        self.assertIn("## 行业影响", prompt)
        self.assertIn("不得预测行业涨跌", prompt)

    def test_heart_prompt_uses_question_and_deterministic_chart(self):
        prompt = _system_prompt("heart")

        self.assertIn("用户在起卦前确定的问题位于calculation.question", prompt)
        self.assertIn("纳甲、卦宫、世应、六亲、六神", prompt)
        self.assertIn("不是商业合作、融资、借贷或寻找资金方", prompt)
        self.assertIn("不得假定用户已经入场", prompt)
        self.assertIn("不得制造外围资金、外围资源", prompt)
        self.assertIn("不得输出使用竖线分栏的Markdown表格", prompt)
        self.assertIn("不得用旬空、填实、出空或干支日推算", prompt)
        self.assertNotIn("最后三句极短的问心句", prompt)
        self.assertIn("## 所问之答", prompt)
        self.assertIn("## 可验证之处", prompt)

    def test_fortune_validator_requires_industry_section_when_symbols_are_present(self):
        context = {
            "calculation": {
                "industry_symbols": [
                    {"element": "水", "basis": ["中运"], "industries": ["航运"]}
                ]
            }
        }
        answer = "年纲与客主关系已经说明，日辰只作当日触发，最后给出个人观察和制衡动作。" * 2
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer, context)

        _validate_answer("fortune", answer + "行业影响只作航运等水属性行业的传统取象，不预测涨跌。", context)

    def test_trend_validator_rejects_market_story_added_after_casting(self):
        answer = "本卦显示当下偏守，动爻揭示由增益转入受阻，之卦说明变化后应先稳住结构。" * 2
        _validate_answer("trend", answer, {})
        with self.assertRaises(HeavenAgentError):
            _validate_answer("trend", answer + "白酒行业政策可能带来外部压力。", {})

    def test_fortune_validator_rejects_invented_natal_strength(self):
        context = {
            "calculation": {
                "pillars": {"year": "丙午", "month": "乙未", "day": "庚戌"},
                "personal": {
                    "today_relative_to_natal_day_master": {
                        "pillars": {"year": "丙午", "month": "乙未", "day": "庚戌"}
                    }
                },
            }
        }
        answer = "丙午年纲、乙未客主和庚戌日辰形成相互制约，个人合参只说明当日关系，不据此判断命局强弱。" * 2
        _validate_answer("fortune", answer, context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "个人本身偏财较重。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "戊辰年进一步增加土气。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "午中藏丁火，所以内在火气被引动。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "戌为火库，内藏火之余气。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "戌属土、含太阳寒水之气。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "木生火、火生土，因此本命日主受到泄耗。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "戌月进一步加重土气。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "日柱庚戌本身就是金。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("fortune", answer + "戌的寒水属性使气机进一步收敛。", context)

    def test_invalid_first_draft_is_rewritten_before_returning(self):
        invalid = "本卦解释完整，但白酒行业政策和基本面正在改变外部环境，因此当下应偏守。" * 2
        valid = "本卦显示当下偏守，实际动爻揭示由增益转入受阻，之卦说明变化后要先确认结构能否重新承接。" * 2
        with patch(
            "backend.features.heaven.agent.llm_transport.chat_completion",
            side_effect=[
                SimpleNamespace(content=invalid, latency_ms=1),
                SimpleNamespace(content=valid, latency_ms=1),
            ],
        ) as completion:
            result = interpret_heaven(
                "trend", {}, "key", "https://example.invalid", "test-model"
            )

        self.assertEqual(result["answer"], valid)
        self.assertEqual(completion.call_count, 2)
        repair_messages = completion.call_args_list[1].kwargs["messages"]
        self.assertIn("未通过本地一致性校验", repair_messages[-1]["content"])

    def test_heart_validator_rejects_invented_financing_and_timing(self):
        context = {
            "calculation": {
                "question": "关于我心中的这笔交易，此刻最需要看清的机会、阻碍与风险是什么？"
            }
        }
        answer = "本卦说明条件仍有矛盾，世应提示内外预期并不一致，动爻要求先核对退出条件，再看之卦所示的收束方向。" * 2
        _validate_answer("heart", answer, context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("heart", answer + "对方资金提供方会转为合作方。", context)
        with self.assertRaises(HeavenAgentError):
            _validate_answer("heart", answer + "未来三日等到旬空填实后再行动。", context)

    def test_heart_validator_rejects_unstated_position_assumptions(self):
        context = {
            "calculation": {
                "question": "关于我心中的这笔交易，此刻最需要看清的机会、阻碍与风险是什么？"
            }
        }
        answer = "本卦说明条件尚未完全衔接，世应提示内外预期有差异，动爻要求先核对参与条件，再看变化后的风险边界。" * 2
        _validate_answer("heart", answer, context)
        for term in ("持仓", "仓位", "建仓", "入场", "持有", "买入", "卖出", "止损", "止盈"):
            with self.subTest(term=term), self.assertRaises(HeavenAgentError):
                _validate_answer("heart", answer + f"用户当前的{term}安排已经明确。", context)

    def test_heart_validator_allows_position_language_when_question_states_it(self):
        context = {"calculation": {"question": "我已经持仓，这笔交易的止损条件是否清楚？"}}
        answer = "本卦所示的内外条件仍有差异，世应关系提醒先核对持仓依据；动爻显示原判断正在变化，止损条件应以用户已经写下的客观边界为准。" * 2

        _validate_answer("heart", answer, context)

    def test_heart_validator_rejects_invented_resource_paths(self):
        context = {"calculation": {"question": "这笔交易最需要防范什么？"}}
        answer = "本卦说明条件尚未完全衔接，世应提示内外预期有差异，动爻要求先核对参与条件，再看变化后的风险边界。" * 2
        for scenario in (
            "外围资金可能进入。",
            "外围资源进入路径尚不清楚。",
            "资金来源会在之后出现。",
            "资源提供的安排已经成形。",
        ):
            with self.subTest(scenario=scenario), self.assertRaises(HeavenAgentError):
                _validate_answer("heart", answer + scenario, context)

    def test_heart_validator_rejects_markdown_tables(self):
        context = {"calculation": {"question": "这笔交易最需要防范什么？"}}
        answer = "本卦说明条件尚未完全衔接，世应提示内外预期有差异，动爻要求先核对参与条件，再看变化后的风险边界。" * 2
        tables = (
            answer + "\n| 项目 | 解释 |\n| --- | --- |\n| 世爻 | 当前条件 |",
            answer + "\n项目 | 解释\n---|---\n世爻 | 当前条件",
        )
        for table in tables:
            with self.subTest(table=table), self.assertRaises(HeavenAgentError):
                _validate_answer("heart", table, context)

    def test_heart_invalid_first_draft_is_rewritten_before_returning(self):
        context = {"calculation": {"question": "这笔交易最需要防范什么？"}}
        invalid = "本卦提示外围资源进入路径尚未落实，用户应先调整仓位，再等待资金进入。" * 3
        valid = "本卦说明条件尚未完全衔接，世应提示内外预期有差异，动爻要求先核对参与条件，再看变化后的风险边界。" * 2
        with patch(
            "backend.features.heaven.agent.llm_transport.chat_completion",
            side_effect=[
                SimpleNamespace(content=invalid, latency_ms=1),
                SimpleNamespace(content=valid, latency_ms=1),
            ],
        ) as completion:
            result = interpret_heaven(
                "heart", context, "key", "https://example.invalid", "test-model"
            )

        self.assertEqual(result["answer"], valid)
        self.assertEqual(completion.call_count, 2)


if __name__ == "__main__":
    unittest.main()
