"""Tests for horizon.ascii_plot - ASCII chart rendering."""

from __future__ import annotations

import math
from dataclasses import dataclass

import pytest

from horizon.plotting import (
    BandData,
    CalibrationPlotData,
    CurveData,
    HeatmapData,
    HistogramData,
    NormalizedEquityData,
    PlotBundle,
    RollingStatData,
    TradeScatterData,
    TradeScatterPoint,
    UnderwaterData,
    bollinger_bands,
    calibration_plot,
    correlation_heatmap,
    from_backtest,
    histogram,
    monthly_returns_heatmap,
    normalized_equity,
    return_distribution,
    trade_scatter,
    underwater_curve,
)

from horizon.ascii_plot import (
    area_chart,
    bar_chart,
    calibration,
    dashboard,
    heatmap,
    histogram_chart,
    line_chart,
    multi_line,
    scatter,
    sparkline,
)


# ---------------------------------------------------------------------------
# Mock Fill
# ---------------------------------------------------------------------------

@dataclass
class MockFill:
    market_id: str = "m1"
    order_side: str = "Buy"
    price: float = 0.5
    size: float = 10.0
    fee: float = 0.0
    timestamp: float = 1000.0


# ---------------------------------------------------------------------------
# line_chart
# ---------------------------------------------------------------------------

class TestLineChart:
    def test_empty(self):
        out = line_chart([], [])
        assert "(no data)" in out

    def test_basic(self):
        ts = [float(i) for i in range(20)]
        vals = [math.sin(i * 0.5) for i in range(20)]
        out = line_chart(ts, vals, width=40, height=10, title="Sine Wave")
        assert "Sine Wave" in out
        assert "·" in out  # default marker
        lines = out.split("\n")
        assert len(lines) > 10  # title + grid + axis + labels

    def test_constant(self):
        ts = [float(i) for i in range(10)]
        vals = [5.0] * 10
        out = line_chart(ts, vals, width=30, height=8)
        assert "·" in out

    def test_single_point(self):
        out = line_chart([0.0], [1.0], width=20, height=5)
        # Should render without crashing
        assert "│" in out

    def test_custom_marker(self):
        ts = [float(i) for i in range(5)]
        vals = [float(i) for i in range(5)]
        out = line_chart(ts, vals, marker="*")
        assert "*" in out


class TestMultiLine:
    def test_empty(self):
        out = multi_line([])
        assert "(no data)" in out

    def test_two_series(self):
        ts = [float(i) for i in range(20)]
        v1 = [float(i) for i in range(20)]
        v2 = [float(20 - i) for i in range(20)]
        out = multi_line([
            (ts, v1, "Up", "·"),
            (ts, v2, "Down", "×"),
        ], title="Two Lines")
        assert "Two Lines" in out
        assert "Up" in out
        assert "Down" in out
        assert "·" in out
        assert "×" in out


# ---------------------------------------------------------------------------
# area_chart
# ---------------------------------------------------------------------------

class TestAreaChart:
    def test_empty(self):
        out = area_chart([], [])
        assert "(no data)" in out

    def test_basic(self):
        ts = [float(i) for i in range(20)]
        vals = [5.0 + 3.0 * math.sin(i * 0.3) for i in range(20)]
        out = area_chart(ts, vals, width=40, height=10, title="Area")
        assert "Area" in out
        assert "░" in out  # fill char
        assert "▓" in out  # edge char


# ---------------------------------------------------------------------------
# bar_chart
# ---------------------------------------------------------------------------

class TestBarChart:
    def test_empty(self):
        out = bar_chart([], [])
        assert "(no data)" in out

    def test_basic(self):
        out = bar_chart(
            ["Alpha", "Beta", "Gamma"],
            [10.0, -5.0, 20.0],
            title="Returns",
        )
        assert "Returns" in out
        assert "Alpha" in out
        assert "█" in out
        assert "░" in out  # negative bar

    def test_single(self):
        out = bar_chart(["X"], [42.0])
        assert "X" in out
        assert "█" in out


# ---------------------------------------------------------------------------
# histogram_chart
# ---------------------------------------------------------------------------

class TestHistogramChart:
    def test_empty(self):
        h = histogram([])
        out = histogram_chart(h)
        assert "(no data)" in out

    def test_basic(self):
        import random
        random.seed(42)
        vals = [random.gauss(0, 1) for _ in range(200)]
        h = histogram(vals, n_bins=10)
        out = histogram_chart(h, width=50, title="Normal Distribution")
        assert "Normal Distribution" in out
        assert "█" in out
        lines = out.strip().split("\n")
        assert len(lines) >= 10  # title + blank + 10 bins

    def test_stats_displayed(self):
        h = histogram([1.0, 2.0, 3.0, 4.0, 5.0], n_bins=5)
        out = histogram_chart(h)
        assert "mean=" in out


# ---------------------------------------------------------------------------
# heatmap
# ---------------------------------------------------------------------------

class TestHeatmap:
    def test_empty(self):
        hm = HeatmapData((), (), (), "Empty")
        out = heatmap(hm)
        assert "(no data)" in out

    def test_correlation(self):
        hm = correlation_heatmap({
            "A": [1.0, 2.0, 3.0, 4.0],
            "B": [4.0, 3.0, 2.0, 1.0],
            "C": [1.0, 1.5, 2.0, 2.5],
        })
        out = heatmap(hm)
        assert "Correlation Matrix" in out
        assert "A" in out
        assert "B" in out
        assert "Scale:" in out

    def test_monthly(self):
        from datetime import datetime, timezone
        t1 = datetime(2025, 1, 1, tzinfo=timezone.utc).timestamp()
        t2 = datetime(2025, 1, 31, tzinfo=timezone.utc).timestamp()
        t3 = datetime(2025, 6, 1, tzinfo=timezone.utc).timestamp()
        t4 = datetime(2025, 6, 30, tzinfo=timezone.utc).timestamp()
        curve = [(t1, 100.0), (t2, 110.0), (t3, 110.0), (t4, 105.0)]
        hm = monthly_returns_heatmap(curve)
        out = heatmap(hm)
        assert "Monthly Returns" in out
        assert "Jan" in out
        assert "Jun" in out


# ---------------------------------------------------------------------------
# scatter
# ---------------------------------------------------------------------------

class TestScatter:
    def test_empty(self):
        ts = TradeScatterData((), (), ())
        out = scatter(ts)
        assert "(no trades)" in out

    def test_basic(self):
        fills = [
            MockFill(order_side="Buy", price=0.4, size=10, timestamp=1.0),
            MockFill(order_side="Sell", price=0.6, size=10, timestamp=2.0),
            MockFill(order_side="Buy", price=0.5, size=5, timestamp=3.0),
            MockFill(order_side="Sell", price=0.7, size=5, timestamp=4.0),
        ]
        ts = trade_scatter(fills)
        out = scatter(ts, width=40, height=10)
        assert "▲" in out  # buy
        assert "▼" in out  # sell
        assert "buy" in out
        assert "sell" in out


# ---------------------------------------------------------------------------
# calibration
# ---------------------------------------------------------------------------

class TestCalibrationChart:
    def test_empty(self):
        c = CalibrationPlotData((), (), (), (), 0.0, 0.0)
        out = calibration(c)
        assert "(no data)" in out

    def test_basic(self):
        fills = [
            MockFill(market_id="m1", order_side="Buy", price=0.3, timestamp=1.0),
            MockFill(market_id="m2", order_side="Buy", price=0.7, timestamp=2.0),
            MockFill(market_id="m3", order_side="Buy", price=0.9, timestamp=3.0),
        ]
        outcomes = {"m1": 0.0, "m2": 1.0, "m3": 1.0}
        cal_data = calibration_plot(fills, outcomes, n_bins=5)
        out = calibration(cal_data, width=30, height=12)
        assert "·" in out  # perfect line
        assert "●" in out  # actual points
        assert "ECE" in out
        assert "Brier" in out


# ---------------------------------------------------------------------------
# sparkline
# ---------------------------------------------------------------------------

class TestSparkline:
    def test_empty(self):
        assert sparkline([]) == ""

    def test_ascending(self):
        out = sparkline([1, 2, 3, 4, 5], width=5)
        assert len(out) == 5
        assert out[0] == "▁"
        assert out[-1] == "█"

    def test_constant(self):
        out = sparkline([3.0, 3.0, 3.0], width=3)
        # All same value → all same char (mapped to index 0 since range=0 → use 1.0)
        assert len(out) == 3
        assert len(set(out)) == 1

    def test_nan_filtered(self):
        out = sparkline([1.0, float("nan"), 3.0, float("inf"), 5.0], width=10)
        assert len(out) > 0
        # Only finite values used
        for ch in out:
            assert ch in "▁▂▃▄▅▆▇█"

    def test_downsampling(self):
        vals = list(range(100))
        out = sparkline(vals, width=10)
        assert len(out) == 10


# ---------------------------------------------------------------------------
# dashboard (full bundle)
# ---------------------------------------------------------------------------

class TestDashboard:
    def _make_bundle(self, n=50, with_trades=True, with_outcomes=False):
        @dataclass
        class MockResult:
            equity_curve: list = None
            trades: list = None
            initial_capital: float = 1000.0
            outcomes: dict = None

        curve = [(float(i), 1000.0 + i * 10.0 - 50.0 * math.sin(i * 0.3)) for i in range(n)]
        trades = []
        outcomes = None

        if with_trades:
            for j in range(5):
                ts = float(j * 10)
                trades.append(MockFill(order_side="Buy", price=0.4, size=10, timestamp=ts))
                trades.append(MockFill(order_side="Sell", price=0.6, size=10, timestamp=ts + 5))

        if with_outcomes:
            outcomes = {"m1": 1.0}

        r = MockResult(equity_curve=curve, trades=trades, outcomes=outcomes)
        return from_backtest(r, rolling_window=10)

    def test_basic_dashboard(self):
        b = self._make_bundle()
        out = dashboard(b, width=60, height=12)
        assert "EQUITY" in out
        assert "DRAWDOWN" in out
        assert "RETURN DISTRIBUTION" in out
        assert "PnL DISTRIBUTION" in out
        assert "Trade Scatter" in out
        assert "═" in out  # separator

    def test_empty_dashboard(self):
        @dataclass
        class EmptyResult:
            equity_curve: list = None
            trades: list = None
            initial_capital: float = 0.0
            outcomes: dict = None

        r = EmptyResult(equity_curve=[], trades=[], outcomes=None)
        b = from_backtest(r)
        out = dashboard(b, width=40)
        # Should not crash, just show separator
        assert "═" in out

    def test_with_calibration(self):
        b = self._make_bundle(with_outcomes=True)
        out = dashboard(b, width=60, height=12)
        # Calibration section should appear
        assert "ECE" in out or "Calibration" in out

    def test_width_respected(self):
        b = self._make_bundle()
        out = dashboard(b, width=50, height=10)
        for line in out.split("\n"):
            # Lines should not vastly exceed width (some labels can extend slightly)
            assert len(line) < 120


# ---------------------------------------------------------------------------
# Integration: plotting dataclasses → ascii_plot
# ---------------------------------------------------------------------------

class TestIntegration:
    def test_curve_data_to_line(self):
        """CurveData fields work directly with line_chart."""
        from horizon.plotting import CurveData
        cd = CurveData(
            timestamps=tuple(float(i) for i in range(20)),
            values=tuple(float(i) * 0.5 for i in range(20)),
            label="Test",
        )
        out = line_chart(cd.timestamps, cd.values, title=cd.label)
        assert "Test" in out

    def test_normalized_equity_to_line(self):
        curve = [(float(i), 100.0 + i) for i in range(30)]
        ne = normalized_equity(curve, initial_capital=100.0)
        out = line_chart(ne.timestamps, ne.equity, title="Equity")
        assert "Equity" in out

    def test_underwater_to_area(self):
        curve = [(float(i), 100.0 - abs(10 - i) * 5) for i in range(20)]
        uw = underwater_curve(curve)
        neg_dd = tuple(-d for d in uw.drawdown_pct)
        out = area_chart(uw.timestamps, neg_dd, title="Underwater")
        assert "Underwater" in out

    def test_histogram_to_chart(self):
        h = histogram([1.0, 2.0, 3.0, 2.5, 1.5, 3.5], n_bins=5)
        out = histogram_chart(h)
        assert "█" in out

    def test_rolling_to_multi_line(self):
        curve = [(float(i), 100.0 + i * 0.5) for i in range(50)]
        from horizon.plotting import rolling_stats
        r = rolling_stats(curve, window=10)
        out = multi_line([
            (r.sharpe.timestamps, r.sharpe.values, r.sharpe.label, "·"),
            (r.sortino.timestamps, r.sortino.values, r.sortino.label, "×"),
        ], title="Risk Metrics")
        assert "Risk Metrics" in out
