"""Verify ratio vectors using algebraic inverse verification."""

from decimal import Decimal, ROUND_DOWN, ROUND_HALF_EVEN

import pytest

from finlint.vectors import VectorFailure, load_suite, run_vector


ROUNDING_MAP = {
    "ROUND_HALF_EVEN": ROUND_HALF_EVEN,
    "ROUND_DOWN": ROUND_DOWN,
}


def compute_ratio(*, op, **kwargs):
    """Dispatch ratio computation by operation type."""
    if op == "divide":
        num = Decimal(kwargs["numerator"])
        den = Decimal(kwargs["denominator"])
        places = kwargs["places"]
        rounding = ROUNDING_MAP[kwargs["rounding"]]
        quantizer = Decimal("1") if places == 0 else Decimal(10) ** -places
        return {"result": (num / den).quantize(quantizer, rounding=rounding)}

    if op == "ratio":
        num = Decimal(kwargs.get("numerator") or kwargs.get("annual_dividend"))
        den = Decimal(kwargs.get("denominator") or kwargs.get("share_price"))
        mult = Decimal(kwargs.get("multiply_by", "1"))
        places = kwargs["places"]
        rounding = ROUNDING_MAP[kwargs["rounding"]]
        quantizer = Decimal(10) ** -places
        return {"result": (num / den * mult).quantize(quantizer, rounding=rounding)}

    if op == "savings_rate":
        income = Decimal(kwargs["income"])
        spending = Decimal(kwargs["spending"])
        places = kwargs["places"]
        rounding = ROUNDING_MAP[kwargs["rounding"]]
        quantizer = Decimal(10) ** -places
        rate = ((income - spending) / income * 100).quantize(quantizer, rounding=rounding)
        return {"result": rate}

    if op == "expense_ratio":
        balance = Decimal(kwargs["balance"])
        rate_pct = Decimal(kwargs["rate_percent"])
        places = kwargs["places"]
        rounding = ROUNDING_MAP[kwargs["rounding"]]
        quantizer = Decimal(10) ** -places
        fee = (balance * rate_pct / 100).quantize(quantizer, rounding=rounding)
        return {"result": fee}

    raise ValueError(f"Unknown op: {op}")


@pytest.fixture(scope="module")
def vectors():
    return load_suite("ratio")


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


def test_ratio_001_emergency_fund(vectors):
    """15750/4500 = 3.5 → ROUND_DOWN → 3 months."""
    v = vectors[0]
    result = compute_ratio(**v["inputs"])
    assert result["result"] == Decimal(v["expected"]["result"])
    # Algebraic inverse: result * denominator <= numerator
    assert Decimal(v["expected"]["result"]) * Decimal(v["inputs"]["denominator"]) <= Decimal(
        v["inputs"]["numerator"]
    )


def test_ratio_002_debt_to_income(vectors):
    """1500/4500 = 33.33% (repeating decimal)."""
    v = vectors[1]
    result = compute_ratio(**v["inputs"])
    assert result["result"] == Decimal(v["expected"]["result"])


def test_ratio_003_savings_rate(vectors):
    """(3200-2700)/3200 = 15.625% → HALF_EVEN → 15.62%."""
    v = vectors[2]
    result = compute_ratio(**v["inputs"])
    assert result["result"] == Decimal(v["expected"]["result"])
    # Algebraic inverse: income * rate/100 ≈ income - spending
    income = Decimal(v["inputs"]["income"])
    spending = Decimal(v["inputs"]["spending"])
    savings = income - spending
    approx_savings = income * Decimal(v["expected"]["result"]) / 100
    assert abs(savings - approx_savings) <= Decimal("0.01") * income / 100


def test_ratio_004_expense_ratio(vectors):
    """0.03% of $500,000 = $150.00."""
    v = vectors[3]
    result = compute_ratio(**v["inputs"])
    assert result["result"] == Decimal(v["expected"]["result"])


def test_ratio_005_dividend_yield(vectors):
    """2.17/52.50 × 100 = 4.1333% (4dp)."""
    v = vectors[4]
    result = compute_ratio(**v["inputs"])
    assert result["result"] == Decimal(v["expected"]["result"])


def test_ratio_006_zero_savings(vectors):
    """Income = spending → 0.00% savings rate."""
    v = vectors[5]
    result = compute_ratio(**v["inputs"])
    assert result["result"] == Decimal(v["expected"]["result"])
    assert result["result"] == Decimal("0")


# --- Blackbox: invariant ---


def test_savings_rate_range(vectors):
    """Savings rate vectors: result must be in [0, 100] when income >= spending."""
    for v in vectors:
        if v["inputs"].get("op") == "savings_rate":
            income = Decimal(v["inputs"]["income"])
            spending = Decimal(v["inputs"]["spending"])
            if income >= spending:
                result = Decimal(v["expected"]["result"])
                assert Decimal("0") <= result <= Decimal("100"), (
                    f"{v['id']}: rate {result} out of range"
                )


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


def test_float_savings_rate_rounds_wrong():
    """Float: round(15.625, 2) gives 15.62 in Python (HALF_EVEN agrees),
    but naive JS Math.round(15.625*100)/100 gives 15.63."""
    # In Python, HALF_EVEN for 15.625 gives 15.62 (2 is even)
    # The vector ensures implementations use HALF_EVEN, not HALF_UP
    from decimal import Decimal

    he_result = Decimal("15.625").quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)
    assert he_result == Decimal("15.62")
    # HALF_UP would give 15.63 (wrong for financial)
    from decimal import ROUND_HALF_UP

    hu_result = Decimal("15.625").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    assert hu_result == Decimal("15.63")


def test_float_wavg_rounds_wrong():
    """Float: round(3.745, 2) gives 3.75, but Decimal HALF_EVEN gives 3.74."""
    assert round(3.745, 2) == 3.75  # Float gives wrong answer
    he_result = Decimal("3.745").quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)
    assert he_result == Decimal("3.74")  # Correct financial rounding


# --- 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 {"result": Decimal("999")}

    with pytest.raises(VectorFailure, match=r"ratio-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 {"result": Decimal("999")}

    with pytest.raises(VectorFailure, match=r"\.result"):
        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 = Decimal(v["expected"]["result"])

    def boundary_impl(**kwargs):
        return {"result": expected + 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 {"wrong_key": Decimal("0")}

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