from __future__ import annotations

import warnings
from decimal import Decimal

import pytest

import fuggers_py.analytics as analytics
from fuggers_py.analytics.error import AnalyticsError as LegacyAnalyticsError
from fuggers_py.analytics.errors import AnalyticsError, InvalidInput
from fuggers_py.analytics.functions import (
    calculate_macaulay_duration as functions_calculate_macaulay_duration,
    calculate_modified_duration as functions_calculate_modified_duration,
    calculate_yield_to_maturity as functions_calculate_yield_to_maturity,
    calculate_z_spread as functions_calculate_z_spread,
)
from fuggers_py.analytics.spreads import z_spread
from fuggers_py.bonds.cashflows.accrued import AccruedInterestCalculator, AccruedInterestInputs
from fuggers_py.bonds.types import YieldCalculationRules, YieldConvention
from fuggers_py.curves import DiscountCurveBuilder
from fuggers_py.core import Date, Price


def test_analytics_root_imports_expose_public_surface() -> None:
    assert analytics.errors.AnalyticsError is AnalyticsError
    assert analytics.AnalyticsError is AnalyticsError
    assert analytics.YieldConvention is YieldConvention
    assert analytics.DurationType is analytics.risk.Duration
    assert analytics.yield_metric is analytics.yields
    assert analytics.engine.StandardYieldEngine is analytics.StandardYieldEngine
    assert analytics.duration.modified_duration is analytics.risk.modified_duration
    assert analytics.accrued.AccruedInterestCalculator is analytics.cashflows.AccruedInterestCalculator


def test_analytics_legacy_error_module_and_root_error_aliases_remain_compatible() -> None:
    assert LegacyAnalyticsError is AnalyticsError

    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        invalid_input = analytics.InvalidInput

    assert invalid_input is InvalidInput
    assert [str(item.message) for item in caught] == [
        "`fuggers_py.analytics.InvalidInput` is deprecated; import it from `fuggers_py.analytics.errors`."
    ]


def test_root_calculate_aliases_match_existing_analytics_functions(fixed_rate_2025_bond) -> None:
    bond = fixed_rate_2025_bond
    settlement = Date.from_ymd(2020, 6, 15)
    price = Price.new(Decimal("105"), bond.currency())

    ytm = analytics.calculate_yield_to_maturity(bond, price, settlement)
    expected_ytm = functions_calculate_yield_to_maturity(bond, price, settlement)
    macaulay = analytics.calculate_macaulay_duration(bond, ytm, settlement)
    modified = analytics.calculate_modified_duration(bond, ytm, settlement)

    assert ytm == expected_ytm
    assert macaulay == functions_calculate_macaulay_duration(bond, ytm, settlement)
    assert modified == functions_calculate_modified_duration(bond, ytm, settlement)


def test_root_calculate_z_spread_alias_matches_existing_spread_function(fixed_rate_2025_bond) -> None:
    bond = fixed_rate_2025_bond
    settlement = Date.from_ymd(2020, 6, 15)
    price = Price.new(Decimal("105"), bond.currency())
    curve = (
        DiscountCurveBuilder(reference_date=settlement)
        .add_zero_rate(1.0, Decimal("0.03"))
        .add_zero_rate(10.0, Decimal("0.03"))
        .build()
    )

    assert analytics.calculate_z_spread(bond, price, curve, settlement) == z_spread(bond, price, curve, settlement)
    assert analytics.calculate_z_spread(bond, price, curve, settlement) == functions_calculate_z_spread(
        bond, price, curve, settlement
    )


def test_calculate_accrued_interest_wrapper_matches_underlying_calculator() -> None:
    rules = YieldCalculationRules.us_corporate()
    inputs = AccruedInterestInputs(
        settlement_date=Date.from_ymd(2024, 3, 15),
        accrual_start=Date.from_ymd(2024, 1, 1),
        accrual_end=Date.from_ymd(2024, 7, 1),
        coupon_amount=Decimal("2.50"),
        coupon_date=Date.from_ymd(2024, 7, 1),
        full_coupon_amount=Decimal("2.50"),
    )

    expected = AccruedInterestCalculator.standard(inputs, rules=rules)
    assert analytics.calculate_accrued_interest(inputs, rules=rules) == expected


def test_existing_submodule_imports_remain_compatible(fixed_rate_2025_bond) -> None:
    bond = fixed_rate_2025_bond
    settlement = Date.from_ymd(2020, 6, 15)
    price = Price.new(Decimal("105"), bond.currency())

    assert analytics.yield_to_maturity(bond, price, settlement) == analytics.calculate_yield_to_maturity(
        bond, price, settlement
    )


def test_analytics_options_module_re_exports_new_greeks_and_rv_helpers() -> None:
    option_greeks = analytics.options.OptionGreeks(delta=Decimal("1.0"), vega=Decimal("2.0"))
    scaled = analytics.options.scale_option_greeks(option_greeks, 2)
    signal = analytics.options.option_rv_signal(
        implied_volatility=Decimal("0.25"),
        realized_volatility=Decimal("0.20"),
        greeks=scaled,
    )

    assert analytics.options.aggregate_option_greeks([option_greeks, scaled]).delta == Decimal("3.0")
    assert signal.volatility_gap == Decimal("0.05")
    assert signal.vega_notional == Decimal("0.04")


def test_analytics_rv_module_re_exports_workflow_and_neutrality_helpers() -> None:
    maturity_signal = analytics.rv.MaturitySignal(name="curve_factor", target_maturity_years=Decimal("5.0"), score=Decimal("1.0"))
    bond_signal = analytics.rv.BondSignal(name="bond_factor", instrument_id="UST5Y", score=Decimal("-1.0"))

    assert maturity_signal.resolved_direction().value == "LONG"
    assert bond_signal.resolved_direction().value == "SHORT"
    assert analytics.rv.NeutralityTarget.DV01.value == "DV01"
