"""
Tests for signalab.account.Account
"""

from __future__ import annotations

import pytest
from conftest import make_account, make_market

from signalab.account import Account, Action, InssufficientMarginError, SimpleMarketData
from signalab.units import Units

# ─────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────

LOT: float = 0.01  # small position, easy maths


def open_buy(account: Account, market: SimpleMarketData, size: float = LOT) -> int:
    account._set_current_market(market)
    return account.open(Action.BUY, size)


def open_sell(account: Account, market: SimpleMarketData, size: float = LOT) -> int:
    account._set_current_market(market)
    return account.open(Action.SELL, size)


def set_market(account: Account, market: SimpleMarketData) -> None:
    account._set_current_market(market)


# ─────────────────────────────────────────────
# Initial state
# ─────────────────────────────────────────────


class TestInitialState:
    def test_initial_balance(self) -> None:
        acc: Account = make_account(balance=5_000.0)
        assert acc.balance == 5_000.0

    def test_no_positions_on_init(self, account: Account) -> None:
        assert account.open_position_count == 0

    def test_used_margin_zero_on_init(self, account: Account) -> None:
        assert account.used_margin() == 0.0

    def test_floating_pl_zero_on_init(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        set_market(account, market)
        assert account.floating_pl() == 0.0

    def test_equity_equals_balance_on_init(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        set_market(account, market)
        assert account.equity() == account.balance


# ─────────────────────────────────────────────
# open()
# ─────────────────────────────────────────────


class TestOpen:
    def test_open_buy_increments_position_count(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        open_buy(account, market)
        assert account.open_position_count == 1

    def test_open_sell_increments_position_count(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        open_sell(account, market)
        assert account.open_position_count == 1

    def test_open_returns_valid_id(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        pid: int = open_buy(account, market)
        assert pid >= 0

    def test_open_deducts_margin(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        value: float = LOT * Units.LOT_SIZE
        expected_margin: float = value / account.leverage
        open_buy(account, market)
        assert account.used_margin() == pytest.approx(expected_margin)

    def test_open_applies_commission(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        initial: float = account.balance
        open_buy(account, market)
        expected_commission: float = Units.open_commission(LOT)
        assert account.balance == pytest.approx(initial + expected_commission)

    def test_open_multiple_positions(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        id1: int = open_buy(account, market)
        id2: int = open_sell(account, market)
        assert id1 != id2
        assert account.open_position_count == 2

    def test_open_increments_buy_count(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        open_buy(account, market)
        assert account.opened_buy_count == 1

    def test_open_increments_sell_count(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        open_sell(account, market)
        assert account.opened_sell_count == 1

    def test_open_insufficient_margin_raises(self) -> None:
        """Regression: open() must raise when margin is insufficient."""
        tiny: Account = make_account(balance=1.0, leverage=1.0)
        market: SimpleMarketData = make_market(1.10000, 1.10010)
        set_market(tiny, market)
        with pytest.raises(InssufficientMarginError):
            tiny.open(Action.BUY, size=1.0)  # 1 lot = $100k margin
        assert tiny.open_position_count == 0

    def test_open_ids_are_sequential(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        ids: list[int] = [open_buy(account, market) for _ in range(5)]
        assert ids == list(range(5))


# ─────────────────────────────────────────────
# close()
# ─────────────────────────────────────────────


class TestClose:
    def test_close_removes_position(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        pid: int = open_buy(account, market)
        set_market(account, market)
        account.close(pid)
        assert account.open_position_count == 0

    def test_close_unknown_id_returns_false(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        set_market(account, market)
        result: bool = account.close(999)
        assert result is False

    def test_close_buy_profit(self) -> None:
        acc: Account = make_account(balance=10_000.0, leverage=100.0)
        open_mkt: SimpleMarketData = make_market(bid=1.10000, ask=1.10010)
        close_mkt: SimpleMarketData = make_market(
            bid=1.10100, ask=1.10110
        )  # +10 pip move
        set_market(acc, open_mkt)
        pid: int = acc.open(Action.BUY, LOT)
        set_market(acc, close_mkt)
        acc.close(pid)
        value: float = LOT * Units.LOT_SIZE
        expected_pl: float = (1.10100 - 1.10010) * value
        assert acc.realized_pl() == pytest.approx(expected_pl, rel=1e-6)

    def test_close_sell_profit(self) -> None:
        acc: Account = make_account(balance=10_000.0, leverage=100.0)
        open_mkt: SimpleMarketData = make_market(bid=1.10000, ask=1.10010)
        close_mkt: SimpleMarketData = make_market(
            bid=1.09900, ask=1.09910
        )  # -10 pip move
        set_market(acc, open_mkt)
        pid: int = acc.open(Action.SELL, LOT)
        set_market(acc, close_mkt)
        acc.close(pid)
        value: float = LOT * Units.LOT_SIZE
        expected_pl: float = (1.10000 - 1.09910) * value
        assert acc.realized_pl() == pytest.approx(expected_pl, rel=1e-6)

    def test_close_buy_loss(self) -> None:
        acc: Account = make_account(balance=10_000.0, leverage=100.0)
        open_mkt: SimpleMarketData = make_market(bid=1.10000, ask=1.10010)
        close_mkt: SimpleMarketData = make_market(
            bid=1.09900, ask=1.09910
        )  # -10 pip against
        set_market(acc, open_mkt)
        pid: int = acc.open(Action.BUY, LOT)
        set_market(acc, close_mkt)
        acc.close(pid)
        value: float = LOT * Units.LOT_SIZE
        expected_pl: float = (1.09900 - 1.10010) * value
        assert acc.realized_pl() == pytest.approx(expected_pl, rel=1e-6)

    def test_close_releases_margin(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        pid: int = open_buy(account, market)
        margin_before: float = account.used_margin()
        assert margin_before > 0
        set_market(account, market)
        account.close(pid)
        assert account.used_margin() == pytest.approx(0.0)

    def test_close_partial_positions(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        pid1: int = open_buy(account, market)
        pid2: int = open_buy(account, market)
        set_market(account, market)
        account.close(pid1)
        assert account.open_position_count == 1
        assert pid2 in account.get_open_position_ids()


# ─────────────────────────────────────────────
# floating_pl() and equity()
# ─────────────────────────────────────────────


class TestFloatingPL:
    def test_floating_pl_buy_positive_move(self) -> None:
        acc: Account = make_account()
        open_mkt: SimpleMarketData = make_market(bid=1.10000, ask=1.10010)
        set_market(acc, open_mkt)
        acc.open(Action.BUY, LOT)
        current: SimpleMarketData = make_market(bid=1.10100, ask=1.10110)
        value: float = LOT * Units.LOT_SIZE
        expected: float = (1.10100 - 1.10010) * value
        set_market(acc, current)
        assert acc.floating_pl() == pytest.approx(expected, rel=1e-6)

    def test_floating_pl_sell_positive_move(self) -> None:
        acc: Account = make_account()
        open_mkt: SimpleMarketData = make_market(bid=1.10000, ask=1.10010)
        set_market(acc, open_mkt)
        acc.open(Action.SELL, LOT)
        current: SimpleMarketData = make_market(bid=1.09900, ask=1.09910)
        value: float = LOT * Units.LOT_SIZE
        expected: float = (1.10000 - 1.09910) * value
        set_market(acc, current)
        assert acc.floating_pl() == pytest.approx(expected, rel=1e-6)

    def test_equity_equals_balance_plus_floating(self) -> None:
        acc: Account = make_account()
        open_mkt: SimpleMarketData = make_market(bid=1.10000, ask=1.10010)
        set_market(acc, open_mkt)
        acc.open(Action.BUY, LOT)
        current: SimpleMarketData = make_market(bid=1.10050, ask=1.10060)
        set_market(acc, current)
        assert acc.equity() == pytest.approx(acc.balance + acc.floating_pl(), rel=1e-9)

    def test_free_margin_equals_equity_minus_used(self) -> None:
        acc: Account = make_account()
        mkt: SimpleMarketData = make_market(1.10000, 1.10010)
        set_market(acc, mkt)
        acc.open(Action.BUY, LOT)
        assert acc.free_margin() == pytest.approx(
            acc.equity() - acc.used_margin(), rel=1e-9
        )


# ─────────────────────────────────────────────
# close_all_positions()
# ─────────────────────────────────────────────


class TestCloseAll:
    def test_close_all_clears_positions(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        open_buy(account, market)
        open_sell(account, market)
        set_market(account, market)
        account.close_all_positions()
        assert account.open_position_count == 0

    def test_close_all_releases_margin(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        open_buy(account, market)
        open_buy(account, market)
        set_market(account, market)
        account.close_all_positions()
        assert account.used_margin() == pytest.approx(0.0)

    def test_close_all_on_empty_account(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        # Should not raise
        set_market(account, market)
        account.close_all_positions()
        assert account.open_position_count == 0


# ─────────────────────────────────────────────
# Liquidation
# ─────────────────────────────────────────────


class TestLiquidation:
    def test_liquidation_closes_positions(self) -> None:
        acc: Account = make_account(
            balance=100.0, leverage=100.0, liquidation_level=0.5
        )
        open_mkt: SimpleMarketData = make_market(bid=1.10000, ask=1.10010)
        set_market(acc, open_mkt)
        acc.open(Action.BUY, LOT)
        crash: SimpleMarketData = make_market(bid=1.00000, ask=1.00010)
        set_market(acc, crash)
        acc.check_liquidation()
        assert acc.open_position_count == 0

    def test_no_liquidation_when_no_positions(
        self, account: Account, market: SimpleMarketData
    ) -> None:
        set_market(account, market)
        account.check_liquidation()
        assert account.open_position_count == 0

    def test_no_liquidation_when_equity_healthy(self) -> None:
        acc: Account = make_account(
            balance=10_000.0, leverage=100.0, liquidation_level=0.5
        )
        mkt: SimpleMarketData = make_market(1.10000, 1.10010)
        set_market(acc, mkt)
        acc.open(Action.BUY, LOT)
        acc.check_liquidation()
        assert acc.open_position_count == 1


# ─────────────────────────────────────────────
# day_change()
# ─────────────────────────────────────────────


class TestDayChange:
    def test_day_change_buy_deducts_cost(self) -> None:
        """Buy positions incur a negative daily swap."""
        acc: Account = make_account()
        mkt: SimpleMarketData = make_market(1.10000, 1.10010)
        set_market(acc, mkt)
        acc.open(Action.BUY, LOT)
        balance_before: float = acc.balance
        acc.day_change(delta=1)
        expected_cost: float = Units.maintain_buy_commission(LOT)
        assert acc.balance == pytest.approx(balance_before + expected_cost, rel=1e-9)

    def test_day_change_sell_earns_credit(self) -> None:
        """Sell positions earn a positive daily swap."""
        acc: Account = make_account()
        mkt: SimpleMarketData = make_market(1.10000, 1.10010)
        set_market(acc, mkt)
        acc.open(Action.SELL, LOT)
        balance_before: float = acc.balance
        acc.day_change(delta=1)
        expected: float = Units.maintain_sell_commission(LOT)
        assert acc.balance == pytest.approx(balance_before + expected, rel=1e-9)

    def test_day_change_delta_scales_linearly(self) -> None:
        """Weekend gaps: delta=2 should cost exactly 2x delta=1."""
        acc1: Account = make_account()
        acc2: Account = make_account()
        mkt: SimpleMarketData = make_market(1.10000, 1.10010)
        set_market(acc1, mkt)
        acc1.open(Action.BUY, LOT)
        set_market(acc2, mkt)
        acc2.open(Action.BUY, LOT)
        b1_before: float = acc1.balance
        b2_before: float = acc2.balance
        acc1.day_change(delta=1)
        acc2.day_change(delta=2)
        cost1: float = acc1.balance - b1_before
        cost2: float = acc2.balance - b2_before
        assert cost2 == pytest.approx(2 * cost1, rel=1e-9)

    def test_day_change_no_positions_no_effect(self, account: Account) -> None:
        balance_before: float = account.balance
        account.day_change(delta=1)
        assert account.balance == balance_before

    def test_day_change_regression_no_plus_one_overcount(self) -> None:
        acc: Account = make_account()
        mkt: SimpleMarketData = make_market(1.10000, 1.10010)
        set_market(acc, mkt)
        acc.open(Action.BUY, LOT)
        balance_before: float = acc.balance
        acc.day_change(delta=3)
        expected: float = Units.maintain_buy_commission(LOT) * 3
        assert acc.balance == pytest.approx(balance_before + expected, rel=1e-9)


# ─────────────────────────────────────────────
# Invariants (property-style)
# ─────────────────────────────────────────────


class TestInvariants:
    def test_equity_invariant_after_sequence(self) -> None:
        """equity == balance + floating_pl after any sequence of opens/closes."""
        acc: Account = make_account()
        mkt1: SimpleMarketData = make_market(1.10000, 1.10010)
        mkt2: SimpleMarketData = make_market(1.10050, 1.10060)
        mkt3: SimpleMarketData = make_market(1.09980, 1.09990)

        set_market(acc, mkt1)
        pid1: int = acc.open(Action.BUY, LOT)
        set_market(acc, mkt2)
        assert acc.equity() == pytest.approx(acc.balance + acc.floating_pl())

        acc.open(Action.SELL, LOT)
        set_market(acc, mkt3)
        assert acc.equity() == pytest.approx(acc.balance + acc.floating_pl())

        acc.close(pid1)
        assert acc.equity() == pytest.approx(acc.balance + acc.floating_pl())

    def test_used_margin_invariant(self) -> None:
        """used_margin must equal sum of per-position margins at all times."""
        acc: Account = make_account(leverage=100.0)
        mkt: SimpleMarketData = make_market(1.10000, 1.10010)

        lot_sizes: list[float] = [0.01, 0.02, 0.05]
        set_market(acc, mkt)
        ids: list[int] = [acc.open(Action.BUY, s) for s in lot_sizes]

        expected_margin: float = sum(
            s * Units.LOT_SIZE / acc.leverage for s in lot_sizes
        )
        assert acc.used_margin() == pytest.approx(expected_margin, rel=1e-9)

        acc.close(ids[1])
        expected_margin = sum(s * Units.LOT_SIZE / acc.leverage for s in [0.01, 0.05])
        assert acc.used_margin() == pytest.approx(expected_margin, rel=1e-9)

    def test_position_pls_sum_equals_floating_pl(self) -> None:
        acc: Account = make_account()
        open_mkt: SimpleMarketData = make_market(1.10000, 1.10010)
        close_mkt: SimpleMarketData = make_market(1.10050, 1.10060)
        set_market(acc, open_mkt)
        acc.open(Action.BUY, 0.01)
        acc.open(Action.SELL, 0.02)
        set_market(acc, close_mkt)
        pls: dict[int, float] = acc.position_pls()
        assert sum(pls.values()) == pytest.approx(acc.floating_pl(), rel=1e-9)
