"""Verify weighted-average vectors using algebraic identity verification."""

from decimal import Decimal, ROUND_HALF_EVEN

import pytest

from finlint.vectors import VectorFailure, load_suite, run_vector


def compute_weighted_average(quantities, prices, places, rounding="ROUND_HALF_EVEN", **kwargs):
    """Compute weighted average: sum(q*p) / sum(q)."""
    from decimal import ROUND_DOWN

    rounding_mode = ROUND_HALF_EVEN if rounding == "ROUND_HALF_EVEN" else ROUND_DOWN
    qs = [Decimal(str(q)) for q in quantities]
    ps = [Decimal(str(p)) for p in prices]
    total_value = sum(q * p for q, p in zip(qs, ps))
    total_qty = sum(qs)
    quantizer = Decimal(10) ** -places
    avg = (total_value / total_qty).quantize(quantizer, rounding=rounding_mode)
    return {
        "weighted_average": avg,
        "total_quantity": total_qty,
        "total_value": total_value,
    }


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


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


def test_wavg_001_cost_basis(vectors):
    """3-lot cost basis: (100×50.25 + 200×49.75 + 150×51.00) / 450 = 50.28."""
    v = vectors[0]
    result = compute_weighted_average(**v["inputs"])
    assert result["weighted_average"] == Decimal(v["expected"]["weighted_average"])
    assert result["total_quantity"] == Decimal(v["expected"]["total_quantity"])
    assert result["total_value"] == Decimal(v["expected"]["total_value"])


def test_wavg_002_boundary(vectors):
    """8.335 on .XX5 boundary → HALF_EVEN → 8.34 (3 is odd, round up)."""
    v = vectors[1]
    result = compute_weighted_average(**v["inputs"])
    assert result["weighted_average"] == Decimal(v["expected"]["weighted_average"])
    # Algebraic identity: avg * total_qty = total_value (within precision)
    avg = result["weighted_average"]
    total_qty = result["total_quantity"]
    total_value = result["total_value"]
    assert abs(avg * total_qty - total_value) <= Decimal("0.01") * total_qty


def test_wavg_003_blended_rate(vectors):
    """Blended interest rate across 3 loans (4dp): 4.8750%."""
    v = vectors[2]
    result = compute_weighted_average(**v["inputs"])
    assert result["weighted_average"] == Decimal(v["expected"]["weighted_average"])


def test_wavg_004_fractional_shares(vectors):
    """Fractional share cost basis with 3 lots."""
    v = vectors[3]
    result = compute_weighted_average(**v["inputs"])
    assert result["weighted_average"] == Decimal(v["expected"]["weighted_average"])
    assert result["total_quantity"] == Decimal(v["expected"]["total_quantity"])


def test_wavg_005_single_lot(vectors):
    """Single lot: must return exact input price."""
    v = vectors[4]
    result = compute_weighted_average(**v["inputs"])
    assert result["weighted_average"] == Decimal(v["expected"]["weighted_average"])
    # Degenerate case: weighted_average must equal the single price exactly
    assert result["weighted_average"] == Decimal(v["inputs"]["prices"][0])


def test_wavg_006_mixed_sign(vectors):
    """Weighted return with mixed pos/neg: 3.745 → HALF_EVEN → 3.74."""
    v = vectors[5]
    result = compute_weighted_average(**v["inputs"])
    assert result["weighted_average"] == Decimal(v["expected"]["weighted_average"])


# --- Blackbox: invariant ---


def test_algebraic_identity(vectors):
    """For all vectors: avg * sum(qty) ≈ sum(qty*price) within precision."""
    for v in vectors:
        if "error" in v["expected"]:
            continue  # Skip error-case vectors (e.g. zero quantity)
        avg = Decimal(v["expected"]["weighted_average"])
        total_qty = Decimal(v["expected"]["total_quantity"])
        total_value = Decimal(v["expected"]["total_value"])
        precision = Decimal(v.get("precision", "0.01"))
        reconstructed = avg * total_qty
        assert abs(reconstructed - total_value) <= precision * total_qty, (
            f"{v['id']}: avg*qty={reconstructed} != total_value={total_value}"
        )


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


def test_float_wavg_006_rounds_wrong():
    """Float: round(3.745, 2) gives 3.75, but HALF_EVEN gives 3.74."""
    float_result = round(3.745, 2)
    assert float_result == 3.75, f"Expected 3.75, got {float_result}"
    decimal_result = Decimal("3.745").quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)
    assert decimal_result == Decimal("3.74")


# --- 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 {
            "weighted_average": Decimal("0"),
            "total_quantity": Decimal("0"),
            "total_value": Decimal("0"),
        }

    with pytest.raises(VectorFailure, match=r"wavg-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 {
            "weighted_average": Decimal("0"),
            "total_quantity": Decimal("0"),
            "total_value": Decimal("0"),
        }

    with pytest.raises(VectorFailure, match=r"\.weighted_average"):
        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_avg = Decimal(v["expected"]["weighted_average"])
    expected_qty = Decimal(v["expected"]["total_quantity"])
    expected_val = Decimal(v["expected"]["total_value"])

    def boundary_impl(**kwargs):
        return {
            "weighted_average": expected_avg + precision,
            "total_quantity": expected_qty,
            "total_value": expected_val,
        }

    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")}  # Missing all expected keys

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