import datetime as dt

import pytest

from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.ports.live_price import FakeLivePrice
from fxhnt.application.paper_book import PaperBookService, persist_paper_book


def test_derive_persist_and_view():
    repo = PaperRepo("sqlite://")
    repo.migrate()
    svc = PaperBookService(repo, capital=100_000.0)
    # one sleeve, weight 0.5, short USDTUSD weight -1.0 at price 2.0
    svc.derive_and_persist(run_date="2026-06-21",
        sleeve_weights={"stablecoin_rotation": 0.5},
        symbol_weights_by_sleeve={"stablecoin_rotation": {"USDTUSD": -1.0}},
        prices={"USDTUSD": 2.0}, at=dt.datetime(2026, 6, 21, tzinfo=dt.UTC))
    view = svc.view(live=FakeLivePrice({"USDTUSD": 1.9}))   # price dropped -> short profits
    assert view.capital == 100_000.0
    assert view.equity > 100_000.0                      # short gained as price fell
    assert any(p.symbol == "USDTUSD" for p in view.positions)
    assert view.sleeves["stablecoin_rotation"].pnl > 0


def _derive(svc, run_date, weight, prices, day):
    svc.derive_and_persist(run_date=run_date,
        sleeve_weights={"s": 1.0},
        symbol_weights_by_sleeve={"s": {"BTC": weight}},
        prices=prices, at=dt.datetime.fromisoformat(day + "T00:00:00+00:00"))


def test_derive_and_persist_is_idempotent_per_run_date():
    repo = PaperRepo("sqlite://")
    repo.migrate()
    svc = PaperBookService(repo, capital=100_000.0)
    # First run from an empty book generates an entry trade for run_date 2026-06-21.
    _derive(svc, "2026-06-21", 1.0, {"BTC": 100.0}, "2026-06-21")
    n1 = len(repo.recent_trades(100))
    assert n1 > 0
    # Re-run the SAME run_date (e.g. cron retry). The append-only path would record the entry trade a
    # second time (prior book is replaced first, so diff is empty on the *second* derive — but the
    # idempotent contract must hold regardless): the run_date's trade count must not grow.
    _derive(svc, "2026-06-21", 1.0, {"BTC": 100.0}, "2026-06-21")
    n2 = len(repo.recent_trades(100))
    assert n2 == n1                                    # same count, NOT doubled


def test_realized_pnl_on_full_exit():
    # Compounding MTM model: the FULL held-book gain (not just a realized sliver) flows into the nav curve,
    # so equity reflects it even after a full exit to cash.
    repo = PaperRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 6, 22, tzinfo=dt.UTC)
    # day 1: long BTC full 100k at 100 -> qty 1000 (sized off equity = capital)
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-21", sleeve_weights={"s": 1.0},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}}, prices={"BTC": 100.0}, at=at)
    # day 2: fully exit (sleeve weight 0) at 110 -> prior book marked: gain 1000*(110-100)=10_000 -> equity 110k, flat
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-22", sleeve_weights={"s": 0.0},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}}, prices={"BTC": 110.0}, at=at)
    view = PaperBookService(repo, capital=100_000.0).view(live=FakeLivePrice({}))   # no open positions
    assert not view.positions
    assert view.equity == pytest.approx(110_000.0)     # full book gain captured by the compounding nav curve


def test_realized_pnl_on_partial_reduction():
    # Compounding MTM model: holding the full book into the up-day captures the whole gain in the nav curve;
    # the reduction still records a realized sliver (now display-only, not the equity driver).
    repo = PaperRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 6, 22, tzinfo=dt.UTC)
    # day 1: long BTC full 100k at 100 -> qty 1000
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-21", sleeve_weights={"s": 1.0},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}}, prices={"BTC": 100.0}, at=at)
    # day 2: reduce to half sleeve weight at 110 -> prior full book (qty 1000) marked: gain 1000*(110-100)=10_000
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-22", sleeve_weights={"s": 0.5},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}}, prices={"BTC": 110.0}, at=at)
    assert repo.realized_pnl() > 0.0                    # reduction still records a realized sliver (display-only)
    view = PaperBookService(repo, capital=100_000.0).view(live=FakeLivePrice({"BTC": 110.0}))
    # equity = compounding nav curve (110k, full book gain) + intraday (0: surviving lot entry=110, marked @110)
    assert view.equity == pytest.approx(110_000.0)


def test_persist_paper_book_writes_nav():
    repo = PaperRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 6, 21, tzinfo=dt.UTC)
    # long BTC full 100k at 100 -> qty 1000; mark date close 120 -> unrealized = 1000*(120-100) = 20_000
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-21",
                       sleeve_weights={"s": 1.0},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}},
                       prices={"BTC": 100.0}, at=at)
    # Recompute the nav at the SAME run_date but with the mark-date close in `prices`.
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-21",
                       sleeve_weights={"s": 1.0},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}},
                       prices={"BTC": 100.0}, at=at)
    hist = repo.nav_history()
    assert hist[-1].run_date == "2026-06-21"
    # entry == close here (positions priced at run-date close), so unrealized 0, realized 0, equity = capital.
    assert hist[-1].equity == pytest.approx(100_000.0)
    assert hist[-1].unrealized == pytest.approx(0.0)


def test_persist_paper_book_nav_unrealized_against_mark():
    repo = PaperRepo("sqlite://")
    repo.migrate()
    at = dt.datetime(2026, 6, 21, tzinfo=dt.UTC)
    # Open at 100 on day 1.
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-20",
                       sleeve_weights={"s": 1.0},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}},
                       prices={"BTC": 100.0}, at=at)
    # Day 2 same target weight; price 110 -> positions re-priced to 110 (entry==mark) so unrealized 0,
    # but realized booked on the rebalance of qty (target qty shrinks as price rises).
    persist_paper_book(repo, capital=100_000.0, run_date="2026-06-21",
                       sleeve_weights={"s": 1.0},
                       symbol_weights_by_sleeve={"s": {"BTC": 1.0}},
                       prices={"BTC": 110.0}, at=at)
    hist = repo.nav_history()
    assert [h.run_date for h in hist] == ["2026-06-20", "2026-06-21"]
    # equity must equal capital + realized + unrealized as computed from persisted positions.
    last = hist[-1]
    assert last.equity == pytest.approx(100_000.0 + last.realized + last.unrealized)
