"""Tests for the vector loader and runner."""

from decimal import Decimal

import pytest

from finlint.vectors import VectorError, VectorFailure, load_suite, run_vector, parse_decimal_values, _compare


def test_load_suite_returns_vectors():
    """load_suite('allocation') returns a list of vector dicts."""
    vectors = load_suite("allocation")
    assert isinstance(vectors, list)
    assert len(vectors) > 0
    assert all("id" in v for v in vectors)


def test_load_suite_not_found_raises():
    """load_suite('nonexistent') raises VectorError."""
    with pytest.raises(VectorError, match="nonexistent"):
        load_suite("nonexistent")


def test_vector_inputs_parsed_as_decimal():
    """String inputs like '100.00' become Decimal when parsed."""
    vectors = load_suite("allocation")
    # The runner should parse these — test via run_vector
    v = vectors[0]

    def impl(amount, parts, method):
        assert isinstance(amount, Decimal), f"amount is {type(amount)}, expected Decimal"
        return {"splits": [Decimal("33.34"), Decimal("33.33"), Decimal("33.33")], "sum": Decimal("100.00")}

    run_vector(v, impl)  # Should not raise


def test_run_vector_passes_correct_implementation():
    """Correct implementation passes without error."""
    vectors = load_suite("allocation")
    v = vectors[0]  # alloc-001: $100.00 ÷ 3

    def correct_impl(amount, parts, method):
        return {
            "splits": [Decimal("33.34"), Decimal("33.33"), Decimal("33.33")],
            "sum": Decimal("100.00"),
        }

    result = run_vector(v, correct_impl)
    assert result is not None


def test_run_vector_fails_incorrect_implementation():
    """Wrong result raises VectorFailure with useful message."""
    vectors = load_suite("allocation")
    v = vectors[0]

    def wrong_impl(amount, parts, method):
        return {
            "splits": [Decimal("33.00"), Decimal("33.00"), Decimal("33.00")],
            "sum": Decimal("99.00"),
        }

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


def test_run_vector_respects_precision_tolerance():
    """Precision tolerance of 0.01 allows small differences."""
    vectors = load_suite("allocation")
    v = vectors[0]

    def close_impl(amount, parts, method):
        # Within 0.01 tolerance
        return {
            "splits": [Decimal("33.34"), Decimal("33.33"), Decimal("33.33")],
            "sum": Decimal("100.005"),  # Within 0.01 of 100.00
        }

    run_vector(v, close_impl)  # Should not raise


def test_load_suite_serialization():
    """load_suite('serialization') returns serialization vectors."""
    vectors = load_suite("serialization")
    assert isinstance(vectors, list)
    assert len(vectors) == 6
    assert vectors[0]["id"] == "serde-001"


# --- Phase 5 whitebox tests ---


def test_vectors_dir_fallback_path_exists():
    """VECTORS_DIR fallback: installed path when monorepo missing."""
    from finlint import vectors as mod
    from pathlib import Path

    # Verify the fallback logic exists
    assert hasattr(mod, '_MONOREPO')
    assert hasattr(mod, '_INSTALLED')
    assert hasattr(mod, 'VECTORS_DIR')
    # _INSTALLED should point to finlint/vectors (sibling of vectors.py)
    assert mod._INSTALLED == Path(mod.__file__).parent / "vectors"


def test_vector_error_message_includes_suite_name():
    """VectorError message includes suite name and path."""
    with pytest.raises(VectorError) as exc_info:
        load_suite("nonexistent_suite_xyz")
    assert "nonexistent_suite_xyz" in str(exc_info.value)
    assert "not found" in str(exc_info.value)


def test_parse_decimal_values_nested_dict_and_list():
    """parse_decimal_values handles nested dict+list recursion."""
    obj = {"a": [{"b": "10.50"}]}
    result = parse_decimal_values(obj)
    assert isinstance(result["a"][0]["b"], Decimal)
    assert result["a"][0]["b"] == Decimal("10.50")


def test_parse_decimal_values_non_numeric_strings_preserved():
    """Non-numeric strings are not converted."""
    assert parse_decimal_values("hello") == "hello"
    assert parse_decimal_values("not_a_number") == "not_a_number"
    assert parse_decimal_values("") == ""


def test_parse_decimal_values_preserves_booleans():
    """Booleans are not converted to Decimal."""
    assert parse_decimal_values(True) is True
    assert parse_decimal_values(False) is False


def test_parse_decimal_values_preserves_integers():
    """Integers are not converted to Decimal."""
    result = parse_decimal_values(42)
    assert result == 42
    assert not isinstance(result, Decimal)


def test_parse_decimal_values_preserves_none():
    """None is preserved."""
    assert parse_decimal_values(None) is None


def test_parse_decimal_values_scientific_notation():
    """Scientific notation strings like '1e-18' are parsed."""
    result = parse_decimal_values("1e-18")
    assert isinstance(result, Decimal)
    assert result == Decimal("1e-18")


def test_compare_expected_dict_actual_not_dict():
    """_compare raises VectorFailure when expected dict but actual is not."""
    with pytest.raises(VectorFailure, match="expected dict"):
        _compare("test-001", {"a": Decimal("1")}, "not_a_dict", Decimal("0.01"))


def test_compare_expected_list_actual_not_list():
    """_compare raises VectorFailure when expected list but actual is not."""
    with pytest.raises(VectorFailure, match="expected list"):
        _compare("test-001", [Decimal("1")], {"a": 1}, Decimal("0.01"))


def test_compare_list_length_mismatch():
    """_compare raises VectorFailure with count on list length mismatch."""
    with pytest.raises(VectorFailure, match="expected 2 items, got 1"):
        _compare("test-001", [Decimal("1"), Decimal("2")], [Decimal("1")], Decimal("0.01"))


def test_compare_decimal_conversion_failure():
    """_compare raises VectorFailure when actual can't be converted to Decimal."""
    with pytest.raises(VectorFailure, match="expected Decimal"):
        _compare("test-001", Decimal("10"), object(), Decimal("0.01"))


def test_compare_boolean_strict_equality():
    """_compare uses equality for booleans."""
    _compare("test-001", True, True, Decimal("0.01"))  # should not raise
    with pytest.raises(VectorFailure):
        _compare("test-001", True, False, Decimal("0.01"))
    # Note: Python bool is subclass of int, so True == 1 is True
    # This is expected Python behavior
    _compare("test-001", True, 1, Decimal("0.01"))  # should not raise


def test_compare_missing_key_in_actual():
    """_compare raises VectorFailure with 'missing' for missing keys."""
    with pytest.raises(VectorFailure, match="missing from result"):
        _compare("test-001", {"a": Decimal("1"), "b": Decimal("2")}, {"a": Decimal("1")}, Decimal("0.01"))


def test_compare_path_tracking_through_nested():
    """_compare includes correct path in error for nested structures."""
    with pytest.raises(VectorFailure, match=r"\.outer\.inner"):
        _compare("test-001", {"outer": {"inner": Decimal("5")}}, {"outer": {"inner": Decimal("99")}}, Decimal("0.01"))


def test_run_vector_non_dict_inputs():
    """run_vector handles non-dict inputs (list)."""
    vector = {
        "id": "test-001",
        "inputs": ["10.00", "20.00"],
        "expected": "30.00",
        "precision": "0.01",
    }
    impl = lambda inputs: sum(inputs)
    result = run_vector(vector, impl)
    assert result == Decimal("30")


def test_run_vector_default_precision_fallback():
    """run_vector defaults precision to 0.01 when not specified."""
    vector = {
        "id": "test-001",
        "inputs": {"value": "10.00"},
        "expected": {"result": "10.00"},
        # no precision field
    }
    # Within 0.01
    impl_close = lambda value: {"result": Decimal("10.005")}
    run_vector(vector, impl_close)  # should not raise

    # Outside 0.01
    impl_far = lambda value: {"result": Decimal("10.02")}
    with pytest.raises(VectorFailure):
        run_vector(vector, impl_far)
