"""
Issue #8 & #9 单元测试

Issue #8: 滚动 IC 均值算法修复（_calc_ic_series_data 中的 rolling 逻辑）
Issue #9: jaccard_overlap_matrix 新增 Overlap 矩阵字段
"""

import pytest
import polars as pl
from datetime import date

from ldnode.calculator.operators.correlation import jaccard_overlap_matrix
from ldnode.calculator.schemas.enumerations import SecurityType


# ---------------------------------------------------------------------------
# 辅助函数
# ---------------------------------------------------------------------------

def compute_rolling(ic_list: list, rolling_period: int) -> list:
    """复制被测算法逻辑（与 _calc_ic_series_data 中 rolling 段完全一致）"""
    rolling = []
    n = len(ic_list)
    for i in range(n):
        window = ic_list[max(0, i - rolling_period + 1): i + 1]
        if len(window) < rolling_period:
            rolling.append(None)
        else:
            total = sum(v if v is not None else 0.0 for v in window)
            rolling.append(total / rolling_period)
    return rolling


def make_res_df(dates_holdings: list[tuple[str, list[str] | None]]) -> pl.DataFrame:
    """构造包含 trade_date 和 hold_securities 列的 res_df"""
    dates = [date.fromisoformat(d) for d, _ in dates_holdings]
    holdings = [h for _, h in dates_holdings]
    return pl.DataFrame({
        "trade_date": pl.Series(dates, dtype=pl.Date),
        "hold_securities": pl.Series(holdings, dtype=pl.List(pl.Utf8)),
    })


# ---------------------------------------------------------------------------
# Issue #8：滚动 IC 均值算法测试
# ---------------------------------------------------------------------------

class TestRollingICMean:
    """Issue #8：测试 _calc_ic_series_data 中的 rolling 均值计算逻辑"""

    def test_normal_sequence_no_none_period3(self):
        """正常序列无 None，period=3：前2个为 None，后3个为固定窗口均值"""
        ic_list = [0.1, 0.2, 0.3, 0.4, 0.5]
        result = compute_rolling(ic_list, rolling_period=3)

        assert result[0] is None
        assert result[1] is None
        assert result[2] == pytest.approx((0.1 + 0.2 + 0.3) / 3)
        assert result[3] == pytest.approx((0.2 + 0.3 + 0.4) / 3)
        assert result[4] == pytest.approx((0.3 + 0.4 + 0.5) / 3)

    def test_contains_none_period3(self):
        """含 None：[0.6, None, 0.4]，period=3 → 前2个 None，第3个=(0.6+0+0.4)/3"""
        ic_list = [0.6, None, 0.4]
        result = compute_rolling(ic_list, rolling_period=3)

        assert result[0] is None
        assert result[1] is None
        assert result[2] == pytest.approx((0.6 + 0.0 + 0.4) / 3)

    def test_all_none_period3(self):
        """全 None：period=3 → 前2个 None，第3个=0.0"""
        ic_list = [None, None, None]
        result = compute_rolling(ic_list, rolling_period=3)

        assert result[0] is None
        assert result[1] is None
        assert result[2] == pytest.approx(0.0)

    def test_boundary_period1_with_none(self):
        """边界 period=1：[None, 0.5] → [0.0, 0.5]（None→0.0，非 None 原值）"""
        ic_list = [None, 0.5]
        result = compute_rolling(ic_list, rolling_period=1)

        assert result[0] == pytest.approx(0.0)
        assert result[1] == pytest.approx(0.5)

    def test_single_element_period1(self):
        """单元素 period=1：[0.3] → [0.3]"""
        ic_list = [0.3]
        result = compute_rolling(ic_list, rolling_period=1)

        assert len(result) == 1
        assert result[0] == pytest.approx(0.3)


# ---------------------------------------------------------------------------
# Issue #9：jaccard_overlap_matrix 新增 Overlap 字段测试
# ---------------------------------------------------------------------------

class TestJaccardOverlapMatrix:
    """Issue #9：测试 jaccard_overlap_matrix 的 jaccard 和 overlap 双矩阵输出"""

    def test_single_strategy_returns_diagonal_one(self):
        """单策略（n=1）→ labels=["A"], jaccard=[[1.0]], overlap=[[1.0]]"""
        res_df_map = {
            "A": make_res_df([("2024-01-01", ["000001", "000002"])])
        }
        security_type_map = {"A": SecurityType.STK}

        result = jaccard_overlap_matrix(res_df_map, security_type_map)

        assert result["labels"] == ["A"]
        assert result["jaccard"] == [[1.0]]
        assert result["overlap"] == [[1.0]]

    def test_empty_res_df_map_returns_empty(self):
        """空 res_df_map（n=0）→ labels=[], jaccard=[], overlap=[]"""
        result = jaccard_overlap_matrix({}, {})

        assert result["labels"] == []
        assert result["jaccard"] == []
        assert result["overlap"] == []

    def test_same_type_completely_disjoint(self):
        """同品种两策略，完全不重叠 → jaccard=0.0, overlap=0.0"""
        res_df_map = {
            "A": make_res_df([("2024-01-01", ["000001", "000002"])]),
            "B": make_res_df([("2024-01-01", ["000003", "000004"])]),
        }
        security_type_map = {"A": SecurityType.STK, "B": SecurityType.STK}

        result = jaccard_overlap_matrix(res_df_map, security_type_map)

        assert result["jaccard"][1][0] == pytest.approx(0.0)
        assert result["overlap"][1][0] == pytest.approx(0.0)

    def test_same_type_completely_overlapping(self):
        """同品种两策略，完全重叠 → jaccard=1.0, overlap=1.0"""
        res_df_map = {
            "A": make_res_df([("2024-01-01", ["000001", "000002"])]),
            "B": make_res_df([("2024-01-01", ["000001", "000002"])]),
        }
        security_type_map = {"A": SecurityType.STK, "B": SecurityType.STK}

        result = jaccard_overlap_matrix(res_df_map, security_type_map)

        assert result["jaccard"][1][0] == pytest.approx(1.0)
        assert result["overlap"][1][0] == pytest.approx(1.0)

    def test_same_type_partial_overlap(self):
        """同品种两策略 A={1,2,3}, B={2,3,4} → jaccard=2/4=0.5, overlap=2/3≈0.667"""
        res_df_map = {
            "A": make_res_df([("2024-01-01", ["s1", "s2", "s3"])]),
            "B": make_res_df([("2024-01-01", ["s2", "s3", "s4"])]),
        }
        security_type_map = {"A": SecurityType.STK, "B": SecurityType.STK}

        result = jaccard_overlap_matrix(res_df_map, security_type_map)

        # intersection=2, union={s1,s2,s3,s4}=4 → jaccard=2/4=0.5
        assert result["jaccard"][1][0] == pytest.approx(0.5)
        # intersection=2, min(3,3)=3 → overlap=2/3
        assert result["overlap"][1][0] == pytest.approx(2.0 / 3.0)

    def test_cross_security_type_returns_zero(self):
        """跨品种策略对 → jaccard=0.0, overlap=0.0"""
        res_df_map = {
            "stk_a": make_res_df([("2024-01-01", ["000001", "000002"])]),
            "cb_a": make_res_df([("2024-01-01", ["110001", "110002"])]),
        }
        security_type_map = {"stk_a": SecurityType.STK, "cb_a": SecurityType.CB}

        result = jaccard_overlap_matrix(res_df_map, security_type_map)

        assert result["jaccard"][1][0] == pytest.approx(0.0)
        assert result["overlap"][1][0] == pytest.approx(0.0)

    def test_hold_securities_none_does_not_raise(self):
        """hold_securities 为 null（Python None）→ 不抛 TypeError，intersection=0"""
        res_df_map = {
            "A": make_res_df([("2024-01-01", None)]),
            "B": make_res_df([("2024-01-01", ["000001"])]),
        }
        security_type_map = {"A": SecurityType.STK, "B": SecurityType.STK}

        # 不应抛出 TypeError
        result = jaccard_overlap_matrix(res_df_map, security_type_map)

        # None 视为空集，intersection=0，union={000001}=1 → jaccard=0.0
        assert result["jaccard"][1][0] == pytest.approx(0.0)
        # min(0, 1)=0 → overlap=0.0
        assert result["overlap"][1][0] == pytest.approx(0.0)
