from __future__ import annotations

import pytest
from psa_core import (
    ObservationRow,
    PortfolioObservation,
    PriceSegment,
    StrategySpec,
    TimeSegment,
    build_rows_from_ranges,
    evaluate_point,
    evaluate_portfolio,
    evaluate_rows,
    evaluate_rows_from_ranges,
)
from psa_core.math import compute_time_coefficient


def _bear_strategy() -> StrategySpec:
    return StrategySpec(
        market_mode="bear",
        price_segments=(
            PriceSegment(price_low=50_000, price_high=60_000, weight=10),
            PriceSegment(price_low=40_000, price_high=50_000, weight=30),
            PriceSegment(price_low=30_000, price_high=40_000, weight=40),
            PriceSegment(price_low=25_000, price_high=30_000, weight=20),
        ),
        time_segments=(
            TimeSegment(
                start_ts="2026-01-01T00:00:00Z",
                end_ts="2026-06-01T00:00:00Z",
                k_start=1.0,
                k_end=1.8,
            ),
        ),
    )


def _bull_strategy() -> StrategySpec:
    return StrategySpec(
        market_mode="bull",
        price_segments=(
            PriceSegment(price_low=25_000, price_high=35_000, weight=20),
            PriceSegment(price_low=35_000, price_high=50_000, weight=30),
            PriceSegment(price_low=50_000, price_high=70_000, weight=50),
        ),
        time_segments=(
            TimeSegment(
                start_ts="2026-01-01T00:00:00Z",
                end_ts="2026-12-31T00:00:00Z",
                k_start=1.0,
                k_end=1.4,
            ),
        ),
    )


def _portfolio_strategy() -> StrategySpec:
    return StrategySpec(
        market_mode="bear",
        price_segments=(PriceSegment(price_low=20_000, price_high=80_000, weight=100),),
        time_segments=(),
    )


def test_piecewise_share_is_monotonic_by_price_for_both_modes() -> None:
    bear = _bear_strategy()
    bull = _bull_strategy()

    bear_high = evaluate_point(bear, "2026-03-01T00:00:00Z", 58_000)
    bear_low = evaluate_point(bear, "2026-03-01T00:00:00Z", 32_000)
    assert bear_low.base_share > bear_high.base_share

    bull_low = evaluate_point(bull, "2026-09-01T00:00:00Z", 32_000)
    bull_high = evaluate_point(bull, "2026-09-01T00:00:00Z", 65_000)
    assert bull_low.base_share > bull_high.base_share


def test_time_coefficient_boundaries_and_midpoint() -> None:
    time_segments = (
        TimeSegment(
            start_ts="2026-01-01T00:00:00Z",
            end_ts="2026-01-11T00:00:00Z",
            k_start=1.0,
            k_end=2.0,
        ),
    )

    assert compute_time_coefficient("2025-12-31T00:00:00Z", time_segments) == 1.0
    assert abs(compute_time_coefficient("2026-01-06T00:00:00Z", time_segments) - 1.5) < 1e-9
    assert compute_time_coefficient("2026-01-20T00:00:00Z", time_segments) == 2.0


def test_market_mode_changes_virtual_price_direction() -> None:
    bear = _bear_strategy()
    bull = _bull_strategy()

    bear_row = evaluate_point(bear, "2026-03-01T00:00:00Z", 42_000)
    bull_row = evaluate_point(bull, "2026-09-01T00:00:00Z", 56_000)

    assert bear_row.virtual_price < bear_row.price
    assert bull_row.virtual_price > bull_row.price
    assert bear_row.target_share >= bear_row.base_share
    assert bull_row.target_share <= bull_row.base_share


def test_evaluate_rows_is_deterministic_and_preserves_order() -> None:
    strategy = _bear_strategy()
    rows = [
        ObservationRow(timestamp="2026-02-01T00:00:00Z", price=47_000),
        ObservationRow(timestamp="2026-03-01T00:00:00Z", price=44_000),
        ObservationRow(timestamp="2026-04-01T00:00:00Z", price=41_000),
    ]

    first = evaluate_rows(strategy, rows)
    second = evaluate_rows(strategy, rows)

    assert first == second
    assert [row.timestamp for row in first] == [row.timestamp for row in rows]


def test_build_rows_from_ranges_includes_price_breakpoints() -> None:
    strategy = _bear_strategy()
    rows = build_rows_from_ranges(
        strategy,
        price_start=60_000,
        price_end=25_000,
        price_steps=4,
        time_start="2026-02-01T00:00:00Z",
        time_end="2026-04-01T00:00:00Z",
        time_steps=3,
        include_price_breakpoints=True,
    )

    prices_first_ts = [row.price for row in rows if row.timestamp == rows[0].timestamp]
    assert 50_000 in prices_first_ts
    assert 40_000 in prices_first_ts
    assert 30_000 in prices_first_ts
    assert prices_first_ts == sorted(prices_first_ts, reverse=True)
    assert len(rows) == 3 * len(prices_first_ts)


def test_evaluate_rows_from_ranges_calls_evaluate_rows_flow() -> None:
    strategy = _bear_strategy()
    evaluated = evaluate_rows_from_ranges(
        strategy,
        price_start=60_000,
        price_end=25_000,
        price_steps=4,
        time_start="2026-02-01T00:00:00Z",
        time_end="2026-04-01T00:00:00Z",
        time_steps=3,
        include_price_breakpoints=True,
    )

    assert len(evaluated) > 0
    assert all(0 <= row.base_share <= 1 for row in evaluated)
    assert all(0 <= row.target_share <= 1 for row in evaluated)


def test_build_rows_from_ranges_rejects_non_integer_steps() -> None:
    strategy = _bear_strategy()

    with pytest.raises(ValueError, match="price_steps must be an integer >= 1"):
        build_rows_from_ranges(
            strategy,
            price_start=60_000,
            price_end=25_000,
            price_steps=2.9,  # type: ignore[arg-type]
            time_start="2026-02-01T00:00:00Z",
            time_end="2026-04-01T00:00:00Z",
            time_steps=3,
        )

    with pytest.raises(ValueError, match="time_steps must be an integer >= 1"):
        build_rows_from_ranges(
            strategy,
            price_start=60_000,
            price_end=25_000,
            price_steps=3,
            time_start="2026-02-01T00:00:00Z",
            time_end="2026-04-01T00:00:00Z",
            time_steps=1.9,  # type: ignore[arg-type]
        )


def test_build_rows_from_ranges_rejects_bool_steps() -> None:
    strategy = _bear_strategy()

    with pytest.raises(ValueError, match="price_steps must be an integer >= 1"):
        build_rows_from_ranges(
            strategy,
            price_start=60_000,
            price_end=25_000,
            price_steps=True,  # type: ignore[arg-type]
            time_start="2026-02-01T00:00:00Z",
            time_end="2026-04-01T00:00:00Z",
            time_steps=3,
        )

    with pytest.raises(ValueError, match="time_steps must be an integer >= 1"):
        build_rows_from_ranges(
            strategy,
            price_start=60_000,
            price_end=25_000,
            price_steps=3,
            time_start="2026-02-01T00:00:00Z",
            time_end="2026-04-01T00:00:00Z",
            time_steps=False,  # type: ignore[arg-type]
        )


def test_evaluate_portfolio_computes_core_fields() -> None:
    strategy = _bear_strategy()
    observation = PortfolioObservation(
        timestamp="2026-03-01T00:00:00Z",
        price=42_000,
        usd_amount=15_000,
        asset_amount=0.5,
        avg_entry_price=38_000,
    )

    evaluated = evaluate_portfolio(strategy, observation)
    expected_portfolio_value = observation.usd_amount + observation.asset_amount * observation.price
    expected_asset_value = observation.asset_amount * observation.price
    expected_base_share = expected_asset_value / expected_portfolio_value
    expected_target_asset_amount = (
        evaluated.target_share * expected_portfolio_value
    ) / observation.price

    assert evaluated.portfolio_value_usd == pytest.approx(expected_portfolio_value)
    assert evaluated.asset_value_usd == pytest.approx(expected_asset_value)
    assert evaluated.usd_value_usd == pytest.approx(observation.usd_amount)
    assert evaluated.base_share == pytest.approx(expected_base_share)
    assert evaluated.share_deviation == pytest.approx(evaluated.base_share - evaluated.target_share)
    assert evaluated.target_asset_amount == pytest.approx(expected_target_asset_amount)
    assert evaluated.usd_delta == pytest.approx(-evaluated.asset_amount_delta * observation.price)
    assert evaluated.avg_entry_pnl_usd == pytest.approx(observation.asset_amount * 4_000)
    assert evaluated.avg_entry_pnl_pct == pytest.approx((42_000 / 38_000) - 1.0)


def test_evaluate_portfolio_avg_entry_fields_are_null_when_not_provided() -> None:
    strategy = _bear_strategy()
    observation = PortfolioObservation(
        timestamp="2026-03-01T00:00:00Z",
        price=42_000,
        usd_amount=15_000,
        asset_amount=0.5,
    )

    evaluated = evaluate_portfolio(strategy, observation)
    assert evaluated.avg_entry_price is None
    assert evaluated.avg_entry_pnl_usd is None
    assert evaluated.avg_entry_pnl_pct is None


def test_evaluate_portfolio_returns_alignment_price_when_root_exists() -> None:
    strategy = _portfolio_strategy()
    observation = PortfolioObservation(
        timestamp="2026-03-01T00:00:00Z",
        price=40_000,
        usd_amount=40_000,
        asset_amount=1.0,
    )

    evaluated = evaluate_portfolio(strategy, observation)
    assert evaluated.alignment_price is not None
    assert evaluated.alignment_price > 0


def test_evaluate_portfolio_returns_null_alignment_when_no_root_in_search_range() -> None:
    strategy = _portfolio_strategy()
    observation = PortfolioObservation(
        timestamp="2026-03-01T00:00:00Z",
        price=40_000,
        usd_amount=500_000,
        asset_amount=0.2,
        alignment_search_min_price=2_000,
        alignment_search_max_price=8_000,
    )

    evaluated = evaluate_portfolio(strategy, observation)
    assert evaluated.alignment_price is None


def test_evaluate_portfolio_is_deterministic() -> None:
    strategy = _portfolio_strategy()
    observation = PortfolioObservation(
        timestamp="2026-03-01T00:00:00Z",
        price=40_000,
        usd_amount=12_000,
        asset_amount=0.3,
        avg_entry_price=35_000,
    )

    first = evaluate_portfolio(strategy, observation)
    second = evaluate_portfolio(strategy, observation)
    assert first == second
