# -*- coding: utf-8 -*-
# AI_CHANGELOG
# - 2026-02-14 | bugfix: 修复 3 个测试 bug
#   prompt: "继续。完成后检查所有任务是否全面"
#   直接原因: (1) mock_config.get 返回 None 导致 timezone 异常；(2) _build_daily_record 缺少 gift_card 参数；(3) loaded_at naive/aware 不匹配
#   变更: mock_config.get 改用 side_effect 返回 default；补充 gift_card 参数；loaded_at 改用 aware datetime
#   验证: pytest tests/unit -x（449 passed）
"""
DWS任务单元测试

测试内容：
- BaseDwsTask基类方法
- 时间计算方法
- 配置应用方法
- 排名计算方法
"""

import pytest
from datetime import date, datetime, timedelta
from decimal import Decimal
from unittest.mock import MagicMock, patch

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from tasks.dws.base_dws_task import (
    BaseDwsTask, 
    TimeLayer, 
    TimeWindow, 
    CourseType,
    TimeRange,
    ConfigCache
)
from tasks.dws.finance_daily_task import FinanceDailyTask
from tasks.dws.assistant_monthly_task import AssistantMonthlyTask


class TestTimeLayerRange:
    """测试时间分层范围计算"""
    
    def test_last_2_days(self):
        """测试近2天"""
        base_date = date(2026, 2, 1)
        # 创建一个模拟的BaseDwsTask实例
        task = create_mock_task()
        
        result = task.get_time_layer_range(TimeLayer.LAST_2_DAYS, base_date)
        
        assert result.start == date(2026, 1, 31)
        assert result.end == date(2026, 2, 1)
    
    def test_last_1_month(self):
        """测试近1月"""
        base_date = date(2026, 2, 1)
        task = create_mock_task()
        
        result = task.get_time_layer_range(TimeLayer.LAST_1_MONTH, base_date)
        
        assert result.start == date(2026, 1, 2)
        assert result.end == date(2026, 2, 1)
    
    def test_last_3_months(self):
        """测试近3月"""
        base_date = date(2026, 2, 1)
        task = create_mock_task()
        
        result = task.get_time_layer_range(TimeLayer.LAST_3_MONTHS, base_date)
        
        assert result.start == date(2025, 11, 3)
        assert result.end == date(2026, 2, 1)


class TestTimeWindowRange:
    """测试时间窗口范围计算"""
    
    def test_this_week_monday_start(self):
        """测试本周（周一起始）"""
        # 2026-02-01 是周日
        base_date = date(2026, 2, 1)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.THIS_WEEK, base_date)
        
        # 本周一是 2026-01-26
        assert result.start == date(2026, 1, 26)
        assert result.end == date(2026, 2, 1)
    
    def test_last_week(self):
        """测试上周"""
        base_date = date(2026, 2, 1)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.LAST_WEEK, base_date)
        
        # 上周一是 2026-01-19，上周日是 2026-01-25
        assert result.start == date(2026, 1, 19)
        assert result.end == date(2026, 1, 25)
    
    def test_this_month(self):
        """测试本月"""
        base_date = date(2026, 2, 15)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.THIS_MONTH, base_date)
        
        assert result.start == date(2026, 2, 1)
        assert result.end == date(2026, 2, 15)
    
    def test_last_month(self):
        """测试上月"""
        base_date = date(2026, 2, 15)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.LAST_MONTH, base_date)
        
        assert result.start == date(2026, 1, 1)
        assert result.end == date(2026, 1, 31)
    
    def test_last_3_months_excl_current(self):
        """测试前3个月（不含本月）"""
        base_date = date(2026, 2, 15)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.LAST_3_MONTHS_EXCL_CURRENT, base_date)
        
        assert result.start == date(2025, 11, 1)
        assert result.end == date(2026, 1, 31)
    
    def test_last_3_months_incl_current(self):
        """测试前3个月（含本月）"""
        base_date = date(2026, 2, 15)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.LAST_3_MONTHS_INCL_CURRENT, base_date)
        
        assert result.start == date(2025, 12, 1)
        assert result.end == date(2026, 2, 15)
    
    def test_this_quarter(self):
        """测试本季度"""
        base_date = date(2026, 2, 15)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.THIS_QUARTER, base_date)
        
        assert result.start == date(2026, 1, 1)
        assert result.end == date(2026, 2, 15)
    
    def test_last_6_months(self):
        """测试最近半年（不含本月）"""
        base_date = date(2026, 2, 15)
        task = create_mock_task()
        
        result = task.get_time_window_range(TimeWindow.LAST_6_MONTHS, base_date)
        
        # 不含本月，从上月末往前6个月
        assert result.end == date(2026, 1, 31)
        assert result.start == date(2025, 8, 1)


class TestComparisonRange:
    """测试环比区间计算"""
    
    def test_comparison_7_days(self):
        """测试7天环比"""
        task = create_mock_task()
        current = TimeRange(start=date(2026, 2, 1), end=date(2026, 2, 7))
        
        result = task.get_comparison_range(current)
        
        # 上一个7天：1月25日-1月31日
        assert result.start == date(2026, 1, 25)
        assert result.end == date(2026, 1, 31)
    
    def test_comparison_30_days(self):
        """测试30天环比"""
        task = create_mock_task()
        current = TimeRange(start=date(2026, 2, 1), end=date(2026, 3, 2))
        
        result = task.get_comparison_range(current)
        
        # 上一个30天区间
        assert (result.end - result.start).days == (current.end - current.start).days


class TestFinanceDailyRecord:
    """测试财务日度记录计算"""
    
    def test_groupbuy_and_cashflow(self):
        """测试团购优惠与现金流口径"""
        task = create_finance_daily_task()
        stat_date = date(2026, 2, 1)
        
        settle = {
            'gross_amount': Decimal('1000'),
            'table_fee_amount': Decimal('1000'),
            'goods_amount': Decimal('0'),
            'assistant_pd_amount': Decimal('0'),
            'assistant_cx_amount': Decimal('0'),
            'cash_pay_amount': Decimal('300'),
            'card_pay_amount': Decimal('0'),
            'balance_pay_amount': Decimal('0'),
            'gift_card_pay_amount': Decimal('0'),
            'coupon_amount': Decimal('200'),
            'pl_coupon_sale_amount': Decimal('0'),
            'adjust_amount': Decimal('50'),
            'member_discount_amount': Decimal('10'),
            'rounding_amount': Decimal('0'),
            'order_count': 1,
            'member_order_count': 1,
            'guest_order_count': 0,
        }
        groupbuy = {'groupbuy_pay_total': Decimal('80')}
        recharge = {'recharge_cash': Decimal('20')}
        expense = {'expense_amount': Decimal('40')}
        platform = {
            'settlement_amount': Decimal('60'),
            'commission_amount': Decimal('5'),
            'service_fee': Decimal('5'),
        }
        big_customer = {'big_customer_amount': Decimal('20')}
        
        gift_card = {'gift_card_consume': Decimal('0')}
        record = task._build_daily_record(
            stat_date, settle, groupbuy, recharge, gift_card, expense, platform, big_customer, 1
        )
        
        assert record['discount_groupbuy'] == Decimal('120')
        assert record['discount_other'] == Decimal('30')
        assert record['platform_settlement_amount'] == Decimal('60')
        assert record['platform_fee_amount'] == Decimal('10')
        assert record['cash_inflow_total'] == Decimal('380')
        assert record['cash_outflow_total'] == Decimal('50')
        assert record['cash_balance_change'] == Decimal('330')


class TestNewHireTier:
    """测试新入职定档规则"""
    
    def test_new_hire_tier_hours(self):
        """测试日均*30折算"""
        task = create_assistant_monthly_task()
        effective_hours = Decimal('15')
        work_days = 5
        result = task._calc_new_hire_tier_hours(effective_hours, work_days)
        assert result == Decimal('90')
    
    def test_max_tier_level_cap(self):
        """测试新入职定档上限"""
        task = create_mock_task()
        now = datetime.now(tz=task.tz)
        task._config_cache = ConfigCache(
            performance_tiers=[
                {'tier_id': 1, 'tier_level': 1, 'min_hours': 0, 'max_hours': 100, 'is_new_hire_tier': False, 'effective_from': date(2020, 1, 1), 'effective_to': date(2099, 1, 1)},
                {'tier_id': 2, 'tier_level': 2, 'min_hours': 100, 'max_hours': 200, 'is_new_hire_tier': False, 'effective_from': date(2020, 1, 1), 'effective_to': date(2099, 1, 1)},
                {'tier_id': 3, 'tier_level': 3, 'min_hours': 200, 'max_hours': 300, 'is_new_hire_tier': False, 'effective_from': date(2020, 1, 1), 'effective_to': date(2099, 1, 1)},
                {'tier_id': 4, 'tier_level': 4, 'min_hours': 300, 'max_hours': None, 'is_new_hire_tier': False, 'effective_from': date(2020, 1, 1), 'effective_to': date(2099, 1, 1)},
            ],
            level_prices=[],
            bonus_rules=[],
            area_categories={},
            skill_types={},
            loaded_at=now
        )
        
        tier = task.get_performance_tier(
            Decimal('350'),
            is_new_hire=True,
            effective_date=date(2026, 2, 1),
            max_tier_level=3
        )
        assert tier['tier_level'] == 3


class TestNewHireCheck:
    """测试新入职判断"""
    
    def test_new_hire_in_month(self):
        """测试月内入职为新入职"""
        task = create_mock_task()
        hire_date = date(2026, 2, 5)
        stat_month = date(2026, 2, 1)
        
        assert task.is_new_hire_in_month(hire_date, stat_month) == True
    
    def test_not_new_hire(self):
        """测试月前入职不是新入职"""
        task = create_mock_task()
        hire_date = date(2026, 1, 15)
        stat_month = date(2026, 2, 1)
        
        assert task.is_new_hire_in_month(hire_date, stat_month) == False
    
    def test_hire_on_first_day(self):
        """测试月1日入职为新入职"""
        task = create_mock_task()
        hire_date = date(2026, 2, 1)
        stat_month = date(2026, 2, 1)
        
        assert task.is_new_hire_in_month(hire_date, stat_month) == True


class TestRankWithTies:
    """测试考虑并列的排名计算"""
    
    def test_no_ties(self):
        """测试无并列情况"""
        task = create_mock_task()
        values = [
            (1, Decimal('100')),
            (2, Decimal('90')),
            (3, Decimal('80')),
        ]
        
        result = task.calculate_rank_with_ties(values)
        
        assert result[0] == (1, 1, 1)  # 第1名
        assert result[1] == (2, 2, 2)  # 第2名
        assert result[2] == (3, 3, 3)  # 第3名
    
    def test_with_ties(self):
        """测试有并列情况"""
        task = create_mock_task()
        values = [
            (1, Decimal('100')),
            (2, Decimal('100')),  # 并列第1
            (3, Decimal('80')),
        ]
        
        result = task.calculate_rank_with_ties(values)
        
        # 两个第1，下一个是第3
        assert result[0][1] == 1  # 第1名
        assert result[1][1] == 1  # 并列第1名
        assert result[2][1] == 3  # 第3名（跳过2）
    
    def test_all_ties(self):
        """测试全部并列"""
        task = create_mock_task()
        values = [
            (1, Decimal('100')),
            (2, Decimal('100')),
            (3, Decimal('100')),
        ]
        
        result = task.calculate_rank_with_ties(values)
        
        # 全部第1
        assert all(r[1] == 1 for r in result)


class TestGuestCheck:
    """测试散客判断"""
    
    def test_guest_zero(self):
        """测试member_id=0为散客"""
        task = create_mock_task()
        assert task.is_guest(0) == True
    
    def test_guest_none(self):
        """测试member_id=None为散客"""
        task = create_mock_task()
        assert task.is_guest(None) == True
    
    def test_not_guest(self):
        """测试正常会员不是散客"""
        task = create_mock_task()
        assert task.is_guest(12345) == False


class TestUtilityMethods:
    """测试工具方法"""
    
    def test_safe_decimal(self):
        """测试安全Decimal转换"""
        task = create_mock_task()
        
        assert task.safe_decimal(100) == Decimal('100')
        assert task.safe_decimal('123.45') == Decimal('123.45')
        assert task.safe_decimal(None) == Decimal('0')
        assert task.safe_decimal('invalid') == Decimal('0')
    
    def test_safe_int(self):
        """测试安全int转换"""
        task = create_mock_task()
        
        assert task.safe_int(100) == 100
        assert task.safe_int('123') == 123
        assert task.safe_int(None) == 0
        assert task.safe_int('invalid') == 0
    
    def test_seconds_to_hours(self):
        """测试秒转小时"""
        task = create_mock_task()
        
        assert task.seconds_to_hours(3600) == Decimal('1')
        assert task.seconds_to_hours(5400) == Decimal('1.5')
        assert task.seconds_to_hours(0) == Decimal('0')
    
    def test_hours_to_seconds(self):
        """测试小时转秒"""
        task = create_mock_task()
        
        assert task.hours_to_seconds(Decimal('1')) == 3600
        assert task.hours_to_seconds(Decimal('1.5')) == 5400


class TestCourseType:
    """测试课程类型"""
    
    def test_base_course(self):
        """测试基础课"""
        assert CourseType.BASE.value == 'BASE'
    
    def test_bonus_course(self):
        """测试附加课"""
        assert CourseType.BONUS.value == 'BONUS'


# =============================================================================
# 辅助函数
# =============================================================================

def create_mock_task():
    """
    创建一个模拟的BaseDwsTask实例用于测试
    """
    # 创建一个具体的子类用于测试
    class TestDwsTask(BaseDwsTask):
        def get_task_code(self):
            return "TEST_DWS_TASK"
        
        def get_target_table(self):
            return "test_table"
        
        def get_primary_keys(self):
            return ["id"]
        
        def extract(self, context):
            return {}
        
        def load(self, transformed, context):
            return {}
    
    # 创建模拟的依赖
    mock_config = MagicMock()
    mock_config.get.side_effect = lambda key, default=None: default
    
    mock_db = MagicMock()
    mock_api = MagicMock()
    mock_logger = MagicMock()
    
    task = TestDwsTask(mock_config, mock_db, mock_api, mock_logger)
    return task


def create_finance_daily_task():
    """创建 FinanceDailyTask 实例用于测试"""
    mock_config = MagicMock()
    mock_config.get.side_effect = lambda key, default=None: 1 if key == "app.tenant_id" else default
    mock_db = MagicMock()
    mock_api = MagicMock()
    mock_logger = MagicMock()
    return FinanceDailyTask(mock_config, mock_db, mock_api, mock_logger)


def create_assistant_monthly_task():
    """创建 AssistantMonthlyTask 实例用于测试"""
    mock_config = MagicMock()
    mock_config.get.side_effect = lambda key, default=None: default
    mock_db = MagicMock()
    mock_api = MagicMock()
    mock_logger = MagicMock()
    return AssistantMonthlyTask(mock_config, mock_db, mock_api, mock_logger)


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
