"""字数分配与 rating_json 解析单元测试。"""
import json
import unittest

import config as cfg

from utils import word_allocation as wa


class TestParseRatingJson(unittest.TestCase):
    def test_canonical_items(self):
        raw = json.dumps(
            {
                'items': [
                    {'id': 'T1', 'name': '施工方案', 'weight': 30, 'keywords': ['工艺']},
                    {'id': 'T2', 'name': '质量保证', 'weight': 10, 'keywords': []},
                ],
                'notes': '',
            },
            ensure_ascii=False,
        )
        items = wa.parse_rating_json(raw)
        self.assertEqual(len(items), 2)
        names = {x['name'] for x in items}
        self.assertIn('施工方案', names)
        self.assertIn('质量保证', names)
        wmap = {x['name']: x['weight'] for x in items}
        self.assertEqual(wmap['施工方案'], 30.0)

    def test_malformed_returns_empty(self):
        self.assertEqual(wa.parse_rating_json('not json'), [])
        self.assertEqual(wa.parse_rating_json(''), [])


class TestComputeLeafAllocations(unittest.TestCase):
    def test_none_when_no_rating_and_not_target_pages_budget(self):
        leaves = [{'id': 1, 'section_title': '一、总体方案'}]
        rules = dict(wa.DEFAULT_RULES)
        rules['budget_mode'] = 'anchor_mean'
        self.assertIsNone(
            wa.compute_leaf_allocations('standard', leaves, '', rules)
        )

    def test_uniform_when_no_rating_but_target_pages(self):
        """无技术评分时仍按目标页均分 B=页×每页字，全稿不随节数 N 线性爆量。"""
        leaves = [
            {'id': 1, 'section_title': 'A'},
            {'id': 2, 'section_title': 'B'},
        ]
        rules = dict(wa.DEFAULT_RULES)
        rules['budget_mode'] = 'target_pages'
        old_tp = cfg.TARGET_PAGES
        old_pce = cfg.PAGE_CHAR_ESTIMATE
        try:
            cfg.TARGET_PAGES = 100
            cfg.PAGE_CHAR_ESTIMATE = 700
            out = wa.compute_leaf_allocations('standard', leaves, '', rules)
        finally:
            cfg.TARGET_PAGES = old_tp
            cfg.PAGE_CHAR_ESTIMATE = old_pce
        self.assertIsNotNone(out)
        s = out[1]['target_chars'] + out[2]['target_chars']
        self.assertEqual(s, 100 * 700)
        self.assertEqual(out[1]['target_chars'], out[2]['target_chars'])

    def test_monotonicity_high_weight_match(self):
        rating = json.dumps(
            {
                'items': [
                    {'name': '施工组织设计', 'weight': 50, 'keywords': ['进度']},
                    {'name': '页眉页脚规范', 'weight': 2, 'keywords': []},
                ]
            },
            ensure_ascii=False,
        )
        leaves = [
            {'id': 10, 'section_title': '3.1 施工组织设计与进度计划'},
            {'id': 11, 'section_title': '9.9 页眉格式说明'},
        ]
        rules = dict(wa.DEFAULT_RULES)
        rules['alpha'] = 0.95
        out = wa.compute_leaf_allocations('standard', leaves, rating, rules)
        self.assertIsNotNone(out)
        t_high = out[10]['target_chars']
        t_low = out[11]['target_chars']
        self.assertGreaterEqual(t_high, t_low, '强匹配高分项的章节应不低于弱匹配章节')
        self.assertIn('施工组织设计', out[10]['word_count_spec'])

    def test_budget_anchor_mean(self):
        rating = json.dumps(
            {'items': [{'name': '技术部分', 'weight': 100}]},
            ensure_ascii=False,
        )
        leaves = [
            {'id': 1, 'section_title': 'A'},
            {'id': 2, 'section_title': 'B'},
            {'id': 3, 'section_title': 'C'},
        ]
        rules = dict(wa.DEFAULT_RULES)
        rules['budget_mode'] = 'anchor_mean'
        rules['alpha'] = 0.0
        old_tp = getattr(cfg, 'TARGET_PAGES', 0)
        setattr(cfg, 'TARGET_PAGES', 0)
        try:
            out = wa.compute_leaf_allocations('standard', leaves, rating, rules)
        finally:
            setattr(cfg, 'TARGET_PAGES', old_tp)
        self.assertIsNotNone(out)
        base, core, _, _ = wa.VOLUME_PRESETS['standard']
        expect = int(round(len(leaves) * (base + core) / 2.0))
        s = sum(out[i]['target_chars'] for i in (1, 2, 3))
        self.assertEqual(s, expect)

    def test_budget_target_pages(self):
        rating = json.dumps(
            {'items': [{'name': '技术部分', 'weight': 100}]},
            ensure_ascii=False,
        )
        leaves = [
            {'id': 1, 'section_title': 'A'},
            {'id': 2, 'section_title': 'B'},
        ]
        rules = dict(wa.DEFAULT_RULES)
        rules['budget_mode'] = 'target_pages'
        rules['alpha'] = 0.0
        old_tp = cfg.TARGET_PAGES
        old_pce = cfg.PAGE_CHAR_ESTIMATE
        try:
            cfg.TARGET_PAGES = 100
            cfg.PAGE_CHAR_ESTIMATE = 700
            out = wa.compute_leaf_allocations('standard', leaves, rating, rules)
        finally:
            cfg.TARGET_PAGES = old_tp
            cfg.PAGE_CHAR_ESTIMATE = old_pce
        self.assertIsNotNone(out)
        expect = 100 * 700
        s = sum(out[i]['target_chars'] for i in (1, 2))
        self.assertEqual(s, expect)

    def test_budget_target_pages_falls_back_when_pages_zero(self):
        rating = json.dumps(
            {'items': [{'name': '技术部分', 'weight': 100}]},
            ensure_ascii=False,
        )
        leaves = [
            {'id': 1, 'section_title': 'A'},
            {'id': 2, 'section_title': 'B'},
        ]
        rules = dict(wa.DEFAULT_RULES)
        rules['budget_mode'] = 'target_pages'
        rules['alpha'] = 0.0
        old_tp = cfg.TARGET_PAGES
        try:
            cfg.TARGET_PAGES = 0
            out = wa.compute_leaf_allocations('standard', leaves, rating, rules)
        finally:
            cfg.TARGET_PAGES = old_tp
        self.assertIsNotNone(out)
        base, core, _, _ = wa.VOLUME_PRESETS['standard']
        expect = int(round(len(leaves) * (base + core) / 2.0))
        s = sum(out[i]['target_chars'] for i in (1, 2))
        self.assertEqual(s, expect)

    def test_continuation_threshold(self):
        self.assertEqual(wa.continuation_threshold(2000), 1300)
        self.assertEqual(wa.continuation_threshold(100), 200)


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