from __future__ import annotations

import pandas as pd
import pytest

from signalab.account import Account
from signalab.analytics import metrics
from signalab.analytics.report import AccountReport


def _daily_index(n: int) -> pd.DatetimeIndex:
    return pd.date_range("2024-01-01", periods=n, freq="D")


class TestMetricsFunctions:
    def test_total_return(self) -> None:
        equity = pd.Series([100.0, 110.0, 120.0], index=_daily_index(3))
        assert metrics.total_return(equity) == pytest.approx(0.2)

    def test_cagr(self) -> None:
        equity = pd.Series([100.0, 110.0, 121.0], index=_daily_index(3))
        years = (equity.index[-1] - equity.index[0]).total_seconds() / 31_536_000.0
        expected = (121.0 / 100.0) ** (1.0 / years) - 1.0
        assert metrics.cagr(equity) == pytest.approx(expected)

    def test_max_drawdown(self) -> None:
        equity = pd.Series([100.0, 120.0, 90.0, 130.0], index=_daily_index(4))
        expected = 90.0 / 120.0 - 1.0
        assert metrics.max_drawdown(equity) == pytest.approx(expected)

    def test_sharpe_ratio_zero_for_flat_returns(self) -> None:
        equity = pd.Series([100.0, 100.0, 100.0, 100.0], index=_daily_index(4))
        assert metrics.sharpe_ratio(equity) == 0.0

    def test_sortino_ratio_zero_without_downside(self) -> None:
        equity = pd.Series([100.0, 101.0, 102.0, 103.0], index=_daily_index(4))
        assert metrics.sortino_ratio(equity) == 0.0

    def test_calmar_ratio(self) -> None:
        equity = pd.Series([100.0, 120.0, 90.0, 130.0], index=_daily_index(4))
        expected = metrics.cagr(equity) / abs(metrics.max_drawdown(equity))
        assert metrics.calmar_ratio(equity) == pytest.approx(expected)

    def test_trade_outcome_metrics(self) -> None:
        realized_pl = pd.Series([0.0, 10.0, 5.0, 5.0, 15.0], index=_daily_index(5))

        assert metrics.win_rate(realized_pl) == pytest.approx(2.0 / 3.0)
        assert metrics.profit_factor(realized_pl) == pytest.approx(4.0)
        assert metrics.expectancy(realized_pl) == pytest.approx(5.0)

    def test_exposure(self) -> None:
        used_margin = pd.Series([0.0, 0.0, 10.0, 20.0, 0.0], index=_daily_index(5))
        assert metrics.exposure(used_margin) == pytest.approx(0.4)

    def test_turnover(self) -> None:
        open_buy = pd.Series([0.0, 1.0, 1.0, 2.0], index=_daily_index(4))
        open_sell = pd.Series([0.0, 0.0, 1.0, 1.0], index=_daily_index(4))
        assert metrics.turnover(open_buy, open_sell) == pytest.approx(0.75)


class _DummyAccount(Account):
    def __init__(self, account_df: pd.DataFrame):
        self._account_df = account_df

    def get_record_df(self) -> pd.DataFrame:
        return self._account_df


class TestAccountReportMetricsCache:
    def test_metric_is_computed_lazily_and_cached(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        idx = _daily_index(4)
        account_df = pd.DataFrame(
            {
                "balance": [100.0, 100.0, 100.0, 100.0],
                "equity": [100.0, 110.0, 120.0, 120.0],
                "realized_pl": [0.0, 0.0, 0.0, 0.0],
                "floating_pl": [0.0, 10.0, 20.0, 20.0],
                "open_buy_count": [0.0, 1.0, 1.0, 1.0],
                "open_sell_count": [0.0, 0.0, 0.0, 0.0],
                "used_margin": [0.0, 10.0, 10.0, 10.0],
            },
            index=idx,
        )

        report = AccountReport(_DummyAccount(account_df))

        calls = {"count": 0}

        def fake_total_return(equity: pd.Series) -> float:
            calls["count"] += 1
            return 0.23

        monkeypatch.setattr(metrics, "total_return", fake_total_return)

        assert report.total_return() == pytest.approx(0.23)
        assert report.total_return() == pytest.approx(0.23)
        assert calls["count"] == 1
