"""Verify gain-loss vectors using algebraic inverse verification."""

from decimal import Decimal, ROUND_HALF_EVEN

import pytest

from finlint.vectors import VectorFailure, load_suite, run_vector


def compute_gain(quantity, cost_per_share, current_price):
    """Compute gain amount and percent."""
    q = Decimal(quantity)
    cost = Decimal(cost_per_share)
    current = Decimal(current_price)
    gain_amount = (current - cost) * q
    if cost != 0:
        gain_pct = (gain_amount / (cost * q) * 100).quantize(
            Decimal("0.01"), rounding=ROUND_HALF_EVEN
        )
    else:
        gain_pct = Decimal("0.00")
    return gain_amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN), gain_pct


@pytest.fixture(scope="module")
def vectors():
    return load_suite("gain-loss")


# --- Blackbox: per-vector correctness ---


def test_gain_001_breakeven(vectors):
    """Breakeven: cost = current → gain = 0. Verify: cost + gain = current * qty."""
    v = vectors[0]
    gain_amount, gain_pct = compute_gain(
        v["inputs"]["quantity"], v["inputs"]["cost_per_share"], v["inputs"]["current_price"]
    )
    assert gain_amount == Decimal(v["expected"]["gain_amount"])
    assert gain_pct == Decimal(v["expected"]["gain_percent"])
    # Algebraic inverse: cost_total + gain = current_total
    cost_total = Decimal(v["inputs"]["quantity"]) * Decimal(v["inputs"]["cost_per_share"])
    current_total = Decimal(v["inputs"]["quantity"]) * Decimal(v["inputs"]["current_price"])
    assert cost_total + gain_amount == current_total


def test_gain_002_small_spread(vectors):
    """Small spread × large quantity."""
    v = vectors[1]
    gain_amount, gain_pct = compute_gain(
        v["inputs"]["quantity"], v["inputs"]["cost_per_share"], v["inputs"]["current_price"]
    )
    assert gain_amount == Decimal(v["expected"]["gain_amount"])
    assert gain_pct == Decimal(v["expected"]["gain_percent"])


def test_gain_003_half_even_boundary(vectors):
    """Gain % at .625 boundary → HALF_EVEN → 0.62."""
    v = vectors[2]
    gain_amount, gain_pct = compute_gain(
        v["inputs"]["quantity"], v["inputs"]["cost_per_share"], v["inputs"]["current_price"]
    )
    assert gain_amount == Decimal(v["expected"]["gain_amount"])
    assert gain_pct == Decimal(v["expected"]["gain_percent"])


def test_gain_004_multi_position(vectors):
    """P&L across 3 positions. Verify each position independently."""
    v = vectors[3]
    expected_gains = [Decimal(s) for s in v["expected"]["gains"]]
    total = Decimal("0")
    for i, pos in enumerate(v["inputs"]["positions"]):
        gain, _ = compute_gain(pos["quantity"], pos["cost_per_share"], pos["current_price"])
        assert gain == expected_gains[i], f"Position {i}: expected {expected_gains[i]}, got {gain}"
        total += gain
    assert total == Decimal(v["expected"]["total_gain"])


def test_gain_005_loss(vectors):
    """Loss: current < cost → negative gain."""
    v = vectors[4]
    gain_amount, gain_pct = compute_gain(
        v["inputs"]["quantity"], v["inputs"]["cost_per_share"], v["inputs"]["current_price"]
    )
    assert gain_amount == Decimal(v["expected"]["gain_amount"])
    assert gain_amount < 0  # Must be negative
    assert gain_pct == Decimal(v["expected"]["gain_percent"])


def test_gain_006_zero_gain(vectors):
    """Same buy/sell price → exactly zero gain."""
    v = vectors[5]
    gain_amount, gain_pct = compute_gain(
        v["inputs"]["quantity"], v["inputs"]["cost_per_share"], v["inputs"]["current_price"]
    )
    assert gain_amount == Decimal(v["expected"]["gain_amount"])
    assert gain_amount == Decimal("0")
    assert gain_pct == Decimal("0.00")


# --- Blackbox: invariant ---


def test_gain_sign_invariant(vectors):
    """gain > 0 iff current > cost, gain < 0 iff current < cost, else 0."""
    for v in vectors:
        if "positions" in v["inputs"]:
            continue  # Skip multi-position vector
        qty = Decimal(v["inputs"]["quantity"])
        if qty == 0:
            continue  # Zero quantity always gives zero gain
        cost = Decimal(v["inputs"]["cost_per_share"])
        current = Decimal(v["inputs"]["current_price"])
        gain = Decimal(v["expected"]["gain_amount"])
        if current > cost:
            assert gain > 0, f"{v['id']}: positive expected"
        elif current < cost:
            assert gain < 0, f"{v['id']}: negative expected"
        else:
            assert gain == 0, f"{v['id']}: zero expected"


# --- Blackbox: float produces exact wrong answer ---


def test_float_small_spread_gives_wrong_answer():
    """Float: 1000 * (100.10 - 100.00) = 99.99999999999432, not 100.00."""
    result = 1000 * (100.10 - 100.00)
    assert result != 100.0, "Float should give wrong result for small spread"
    assert abs(result - 100.0) > 1e-12


def test_float_multi_position_gives_wrong_answer():
    """Float: sum of 3 gains gives 44.99999999999993, not 45.00."""
    result = 100 * (50.15 - 50.00) + 200 * (25.10 - 25.00) + 500 * (10.02 - 10.00)
    assert result != 45.0


# --- Whitebox: error reporting ---


def test_wrong_impl_raises_with_vector_id(vectors):
    """VectorFailure message includes vector ID."""
    v = vectors[0]

    def wrong_impl(**kwargs):
        return {"gain_amount": Decimal("999"), "gain_percent": Decimal("999")}

    with pytest.raises(VectorFailure, match=r"gain-001"):
        run_vector(v, wrong_impl)


def test_wrong_impl_raises_with_path(vectors):
    """VectorFailure message includes path to mismatch."""
    v = vectors[0]

    def wrong_impl(**kwargs):
        return {"gain_amount": Decimal("999"), "gain_percent": Decimal("999")}

    with pytest.raises(VectorFailure, match=r"\.gain_amount"):
        run_vector(v, wrong_impl)


def test_precision_boundary(vectors):
    """Result at exactly the tolerance boundary passes (≤ not <)."""
    v = vectors[0]
    precision = Decimal(v["precision"])
    expected_amount = Decimal(v["expected"]["gain_amount"])
    expected_pct = Decimal(v["expected"]["gain_percent"])

    def boundary_impl(**kwargs):
        return {
            "gain_amount": expected_amount + precision,
            "gain_percent": expected_pct + precision,
        }

    run_vector(v, boundary_impl)


def test_missing_key_in_result(vectors):
    """Implementation missing output key raises VectorFailure."""
    v = vectors[0]

    def missing_key_impl(**kwargs):
        return {"gain_amount": Decimal("0")}  # Missing gain_percent

    with pytest.raises(VectorFailure, match=r"missing"):
        run_vector(v, missing_key_impl)
