import numpy as np
import pandas as pd
import pytest

from strategies.mlp_accounting import net_returns, window_metrics


def signals(prices=(100., 90., 90.)):
    return pd.DataFrame(dict(time=pd.date_range('2020-01-01', periods=3, freq='6h'),
        close=prices, low=prices, position=[1, 1, 0],
        execute_entry=[True, False, False], execute_exit=[False, False, True]))


def test_initial_capital_and_exact_fee_timing():
    frame = signals()
    np.testing.assert_allclose(net_returns(frame), [0., -.105, -.005])
    result = window_metrics(frame)
    assert result['pnl_pct'] == pytest.approx(-10.9475)
    assert result['dd_pct'] == pytest.approx(-10.9475)
    assert result['entries'] == result['exits'] == 1
    assert result['exposure_pct'] == pytest.approx(200 / 3)


def test_calendar_duration_changes_cagr():
    frame = signals((100., 102., 101.))
    intraday = window_metrics(frame)
    frame.time = pd.date_range('2020-01-01', periods=3, freq='D')
    daily = window_metrics(frame)
    assert intraday['years'] * 4 == pytest.approx(daily['years'])
    assert 1 + intraday['cagr_pct'] / 100 == pytest.approx((1 + daily['cagr_pct'] / 100) ** 4)


def test_carry_in_and_terminal_open_are_marked_to_market():
    frame = signals((100., 200., 150.))
    frame.position = 1
    frame.execute_exit = False
    result = window_metrics(frame, start='2020-01-01 12:00:00')
    assert result['pnl_pct'] == pytest.approx(-25)
    assert result['entries'] == 0
    assert result['carry_in'] and result['open_at_end']
    assert window_metrics(frame)['pnl_pct'] == pytest.approx(49.625)


def test_date_end_includes_every_intraday_bar_and_inactivity_is_retained():
    frame = signals()
    assert window_metrics(frame, end='2020-01-01')['exits'] == 1
    frame.position = 0
    frame.execute_entry = frame.execute_exit = False
    result = window_metrics(frame)
    assert result['pnl_pct'] == result['dd_pct'] == result['entries'] == 0
    assert result['calmar'] is None


def test_lagged_exposure_and_first_bar_entry():
    frame = signals()
    result = window_metrics(frame, end=pd.Timestamp('2020-01-01'))
    assert result['entries'] == 1
    assert result['exposure_pct'] == 0


def test_shared_metrics_and_report_use_same_accounting():
    from strategies.strategy_activation_scores import calculate_metrics
    from tools.mlp_results_table import run_backtest
    frame = signals()
    research = window_metrics(frame)
    shared = calculate_metrics(frame, min_trades=0)
    report = run_backtest(frame)
    assert shared['Max Drawdown %'] == pytest.approx(research['dd_pct'])
    assert shared['Total Trades'] == 1
    assert report['pnl'] == pytest.approx(research['pnl_pct'])
    assert report['max_dd'] == pytest.approx(research['dd_pct'])


def test_report_keeps_carry_in_and_excludes_open_trade_from_win_rate():
    from tools.mlp_results_table import run_backtest
    frame = signals((100., 200., 150.))
    frame.position = 1
    frame.execute_exit = False
    report = run_backtest(frame, start='2020-01-01 12:00:00')
    assert report['pnl'] == pytest.approx(-25.)
    assert report['total'] == report['wins'] == 0
    assert report['carry_in'] and report['open_at_end']


def test_optimizer_trade_floor_still_penalizes_ratios_but_reports_losses():
    from strategies.strategy_activation_scores import calculate_metrics
    result = calculate_metrics(signals(), min_trades=2)
    assert result['Calmar Ratio'] == result['Sharpe Ratio'] == result['Sortino Ratio'] == -10.
    assert result['Total P&L %'] == pytest.approx(-10.9475)
    assert result['Max Drawdown %'] == pytest.approx(-10.9475)


def test_shared_start_keeps_prior_interval_and_entry_count():
    from strategies.strategy_activation_scores import calculate_metrics
    frame = signals()
    result = calculate_metrics(frame, score_start='2020-01-01 06:00:00', min_trades=0)
    assert result['Total Trades'] == 0
    assert result['Total P&L %'] == pytest.approx(-10.9475)
    assert result['% In Market'] == 100.


def test_duplicate_times_rejected():
    frame = signals()
    frame.loc[1, 'time'] = frame.time.iloc[0]
    with pytest.raises(ValueError, match='strictly increasing'):
        window_metrics(frame)


def test_report_parses_epoch_seconds(tmp_path, monkeypatch):
    import tools.mlp_results_table as report
    frame = signals()
    # Explicit seconds avoid depending on pandas' datetime storage resolution.
    frame.time = [int(stamp.timestamp()) for stamp in pd.date_range('2020-01-01', periods=3, freq='6h')]
    data_path = tmp_path / 'bars.csv'
    frame.to_csv(data_path, index=False)
    winner_path = tmp_path / 'winner.csv'
    pd.DataFrame({'unused': [1]}).to_csv(winner_path, index=False)
    monkeypatch.setattr(report, '_tf_map', lambda asset: {'4H': str(data_path)})
    monkeypatch.setattr(report, 'winner_path', lambda tf, asset: winner_path)
    monkeypatch.setattr(report.strat, 'generate_signals', lambda df, **params: df)
    result = report.evaluate_tf('4H', start='2020-01-01', end='2020-01-01')
    assert result['latest_date'] == '2020-01-01'
    assert result['pnl'] == pytest.approx(-10.9475)


def test_selection_floor_uses_legacy_span_not_return_duration(monkeypatch):
    import strategies.strategy_activation_scores as strategy
    frame = signals()
    frame.time = pd.to_datetime(['2020-01-01', '2021-01-01', '2022-01-01'])
    observed = []
    monkeypatch.setattr(strategy, 'get_min_trades', lambda years: observed.append(years) or 0)
    strategy.calculate_metrics(frame)
    assert observed == [731 / 365.25]
