import os
from datetime import date

import polars as pl
import pytest

os.environ.setdefault("ENDPOINT_URL", "http://localhost")
os.environ.setdefault("ENDPOINT_ACCESS_ID", "test")
os.environ.setdefault("ENDPOINT_ACCESS_TOKEN", "test")

from ldnode.loader.datasources.cb_quotations.realtime_derived_plan import (  # noqa: E402
    calculate_cb_redeem_remain_days,
    plan_cb_realtime_derived_factors,
)
from ldnode.loader.datasources.cb_quotations.ytm_plan import cal_ytm  # noqa: E402


def test_plan_cb_realtime_derived_factors_calculates_pure_value_and_redeem_state():
    valuation_date = date(2026, 6, 17)
    frame = pl.DataFrame(
        {
            "code": ["110073.SH"],
            "conv_end_date": [date(2028, 6, 16)],
            "pay_interest_day_list": [[date(2026, 6, 16), date(2027, 6, 16), date(2028, 6, 16)]],
            "interst_list": [[0.5, 0.7, 1.0]],
            "maturity_put_price": [110.0],
            "cb_ytm": [2.0],
            "redeem_price_rate": [1.3],
            "redeem_count_days": [3],
            "redeem_total_days": [5],
            "conv_start_date": [date(2025, 1, 1)],
            "redeem_trade_date_window": [
                [
                    date(2026, 6, 13),
                    date(2026, 6, 14),
                    date(2026, 6, 15),
                    date(2026, 6, 16),
                ]
            ],
            "stk_close_window": [[100.0, 140.0, 140.0, 100.0]],
            "conv_price_window": [[100.0, 100.0, 100.0, 100.0]],
            "call_event_status_window": [[]],
            "call_event_ann_date_window": [[]],
            "call_event_recal_date_window": [[]],
            "call_event_reg_date_window": [[]],
            "call_event_price_window": [[]],
            "call_event_priority_window": [[]],
            "close_stk": [140.0],
            "conv_price": [100.0],
        }
    )

    result = plan_cb_realtime_derived_factors(frame.lazy(), valuation_date).collect()

    left_years = max((date(2028, 6, 16) - valuation_date).days + 1, 0) / 365
    cashflows = [
        (date(2027, 6, 16), 0.7),
        (date(2028, 6, 16), 110.0),
    ]
    expected_pure_value = sum(
        cash / ((1 + 2.0 / 100) ** ((cashflow_date - valuation_date).days / 365))
        for cashflow_date, cash in cashflows
    )

    assert result["left_years"].item() == pytest.approx(left_years)
    assert result["pure_value"].item() == pytest.approx(expected_pure_value)
    assert result["redeem_real_days"].item() == 3
    assert result["redeem_remain_days"].item() == 0
    assert result["is_call"].item() == "已满足强赎条件"


def test_plan_cb_realtime_derived_factors_respects_active_non_call_event():
    valuation_date = date(2026, 6, 17)
    frame = pl.DataFrame(
        {
            "conv_end_date": [date(2028, 6, 16)],
            "pay_interest_day_list": [[date(2026, 6, 16), date(2027, 6, 16), date(2028, 6, 16)]],
            "interst_list": [[0.5, 0.7, 1.0]],
            "maturity_put_price": [110.0],
            "cb_ytm": [2.0],
            "redeem_price_rate": [1.3],
            "redeem_count_days": [3],
            "redeem_total_days": [5],
            "conv_start_date": [date(2025, 1, 1)],
            "redeem_trade_date_window": [[date(2026, 6, 16)]],
            "stk_close_window": [[140.0]],
            "conv_price_window": [[100.0]],
            "call_event_status_window": [["公告不强赎"]],
            "call_event_ann_date_window": [[date(2026, 6, 1)]],
            "call_event_recal_date_window": [[date(2026, 7, 1)]],
            "call_event_reg_date_window": [[None]],
            "call_event_price_window": [[None]],
            "call_event_priority_window": [[10]],
            "close_stk": [140.0],
            "conv_price": [100.0],
        }
    )

    result = plan_cb_realtime_derived_factors(frame.lazy(), valuation_date).collect()

    assert result["is_call"].item() == "公告不强赎"
    assert result["redeem_real_days"].item() == 0
    assert result["redeem_remain_days"].item() is None


def test_plan_cb_realtime_derived_factors_keeps_empty_window_unstarted_bond_null():
    valuation_date = date(2026, 6, 17)
    frame = pl.DataFrame(
        {
            "conv_end_date": [date(2032, 6, 16)],
            "pay_interest_day_list": [[date(2026, 6, 16), date(2027, 6, 16), date(2028, 6, 16)]],
            "interst_list": [[0.5, 0.7, 1.0]],
            "maturity_put_price": [110.0],
            "cb_ytm": [2.0],
            "redeem_price_rate": [1.3],
            "redeem_count_days": [3],
            "redeem_total_days": [5],
            "conv_start_date": [date(2026, 12, 8)],
            "redeem_trade_date_window": [[]],
            "stk_close_window": [[]],
            "conv_price_window": [[]],
            "call_event_status_window": [[]],
            "call_event_ann_date_window": [[]],
            "call_event_recal_date_window": [[]],
            "call_event_reg_date_window": [[]],
            "call_event_price_window": [[]],
            "call_event_priority_window": [[]],
            "close_stk": [None],
            "conv_price": [None],
        }
    )

    result = plan_cb_realtime_derived_factors(frame.lazy(), valuation_date).collect()

    assert result["is_call"].item() == "未到转股期"
    assert result["redeem_real_days"].item() is None
    assert result["redeem_remain_days"].item() is None


def test_calculate_cb_redeem_remain_days_respects_rolling_window():
    assert calculate_cb_redeem_remain_days(
        redeem_flags=[True, False, True, False],
        redeem_count_days=3,
        redeem_total_days=4,
    ) == 2


def test_cal_ytm_uses_pay_interest_day_cashflows():
    ytm = cal_ytm(
        {
            "conv_end_date": date(2028, 6, 16),
            "pay_interest_day_list": [
                date(2026, 6, 16),
                date(2027, 6, 16),
                date(2028, 6, 16),
            ],
            "interst_list": [0.5, 0.7, 110.0],
            "maturity_put_price": 110.0,
            "close": 100.0,
            "call_event_status_window": [],
            "call_event_reg_date_window": [],
            "call_event_price_window": [],
        },
        date(2026, 6, 17),
    )

    assert ytm is not None
    assert ytm > 0


def test_plan_cb_realtime_derived_factors_noops_before_etl_columns_arrive():
    frame = pl.DataFrame({"code": ["110073.SH"], "pure_value": [100.0]})

    result = plan_cb_realtime_derived_factors(frame.lazy(), date(2026, 6, 17)).collect()

    assert result.columns == ["code", "pure_value"]
    assert result["pure_value"].item() == 100.0


def test_plan_cb_realtime_derived_factors_uses_announced_call_maturity_only():
    valuation_date = date(2026, 6, 17)
    frame = pl.DataFrame(
        {
            "code": ["118018.SH", "127018.SZ"],
            "conv_end_date": [date(2028, 8, 17), date(2026, 6, 28)],
            "pay_interest_day_list": [[date(2026, 6, 17), date(2027, 6, 17), date(2028, 6, 17)], [date(2026, 6, 17), date(2027, 6, 17), date(2028, 6, 17)]],
            "interst_list": [[0.3, 0.5, 1.0], [0.3, 0.5, 1.0]],
            "maturity_put_price": [110.0, 119.0],
            "cb_ytm": [6.793, 1.4676],
            "redeem_price_rate": [1.3, 1.3],
            "redeem_count_days": [15, 15],
            "redeem_total_days": [30, 30],
            "conv_start_date": [date(2024, 1, 1), date(2024, 1, 1)],
            "redeem_trade_date_window": [[], []],
            "stk_close_window": [[], []],
            "conv_price_window": [[], []],
            "call_event_status_window": [["公告实施强赎"], ["公告到期赎回"]],
            "call_event_ann_date_window": [[date(2026, 6, 1)], [date(2026, 5, 25)]],
            "call_event_recal_date_window": [[None], [None]],
            "call_event_reg_date_window": [[date(2026, 7, 3)], [date(2026, 6, 26)]],
            "call_event_price_window": [[101.3233], [119.0]],
            "call_event_priority_window": [[40], [20]],
            "close_stk": [None, None],
            "conv_price": [None, None],
        }
    )

    result = plan_cb_realtime_derived_factors(frame.lazy(), valuation_date).collect()
    announced_call = result.filter(pl.col("code") == "118018.SH").row(0, named=True)
    maturity_call = result.filter(pl.col("code") == "127018.SZ").row(0, named=True)

    assert announced_call["conv_end_date"] == date(2026, 7, 3)
    assert announced_call["maturity_put_price"] == pytest.approx(101.3233)
    assert announced_call["left_years"] == pytest.approx(
        ((date(2026, 7, 3) - valuation_date).days + 1) / 365
    )
    assert maturity_call["conv_end_date"] == date(2026, 6, 26)
    assert maturity_call["maturity_put_price"] == pytest.approx(119.0)
    assert maturity_call["left_years"] == pytest.approx(
        ((date(2026, 6, 26) - valuation_date).days + 1) / 365
    )
