from decimal import Decimal

import pytest

from sigfigs import SigFigNum

# Valid input and normalization

@pytest.mark.parametrize(
    "num",
    [
        "1500",
        "42",
        "0",
        "12.34",
        "0.00420",
        "1.5e3",
        "1.50E+3",
        "2e-4",
        "-12.3",
        "+4.2e5",
        "-3.2E-4",
        "0.5",
        "10.01",
    ],
)
def test_valid_numbers(num):
    """Accept valid integer, decimal, and scientific notation inputs."""
    number = SigFigNum(num)
    assert number.num_str == num.lower()


@pytest.mark.parametrize(
    ("num", "expected"),
    [
        ("12E3", "12e3"),
        ("1.50E-2", "1.50e-2"),
        ("-3.2e4", "-3.2e4"),
        ("1.50E+3", "1.50e+3"),
    ],
)
def test_num_str_normalized(num, expected):
    """Normalize uppercase scientific notation."""
    assert SigFigNum(num).num_str == expected


@pytest.mark.parametrize(
    "num",
    [
        "1500",
        "1.5e3",
        "1.50e3",
        "0.00420",
    ],
)
def test_valid_input_preserves_digit_representation(num):
    """Preserve significant trailing zeros in the normalized string."""
    assert SigFigNum(num).num_str == num


# Invalid input

@pytest.mark.parametrize(
    "num",
    [
        "",
        ".5",
        ".0042",
        "5.",
        "1.",
        ".",
        "1e",
        "1e+",
        "1e-",
        "1.2.3",
        "1..2",
        "12abc",
        "abc12",
        "1 500",
        "--12",
        "++12",
        "+-5",
        "1ee3",
        "1e2e3",
        "nan",
        "NaN",
        "inf",
        "1.2e3.4",
        " 12.3",
        "12.3 ",
        " 12.3 ",
    ],
)
def test_invalid_numbers(num):
    """Reject malformed numeric strings and whitespace."""
    with pytest.raises(ValueError):
        SigFigNum(num)


# Significant figures

@pytest.mark.parametrize(
    ("num", "expected"),
    [
        ("1500", 2),
        ("1000", 1),
        ("1002", 4),
        ("1234", 4),
        ("12.34", 4),
        ("1500.0", 5),
        ("0.0042", 2),
        ("0.00420", 3),
        ("1.50e3", 3),
        ("1.500e3", 4),
        ("1000e0", 1),
        ("0", 1),
        ("0.00", 3),
        ("-12.30", 4),
        ("+5.00", 3),
        ("12E3", 2),
    ],
)
def test_sigfigs(num, expected):
    """Calculate significant figures correctly."""
    assert SigFigNum(num).sigfigs == expected


# Decimal-place position

@pytest.mark.parametrize(
    ("num", "expected"),
    [
        ("1500", -2),
        ("1000", -3),
        ("1002", 0),
        ("1234", 0),
        ("12.34", 2),
        ("12.340", 3),
        ("1500.0", 1),
        ("0.0042", 4),
        ("0.00420", 5),
        ("1.5e3", -2),
        ("1.50e3", -1),
        ("1.500e3", 0),
        ("12.34e2", 0),
        ("12.34e-2", 4),
        ("0.00", 2),
    ],
)
def test_decimals(num, expected):
    """Calculate the signed decimal-place position."""
    assert SigFigNum(num).decimals == expected


# Numerical value

@pytest.mark.parametrize(
    ("num", "expected"),
    [
        ("12", Decimal("12.0")),
        ("-12.5", Decimal("-12.5")),
        ("0.0042", Decimal("0.0042")),
        ("1.5e3", Decimal("1500.0")),
        ("1.50E3", Decimal("1500.0")),
        ("12.34e2", Decimal("1234.0")),
        ("12.34e-2", Decimal("0.1234")),
        ("-2.5e2", Decimal("-250.0")),
        ("0", Decimal("0.0")),
    ],
)
def test_value(num, expected):
    """Calculate the numerical value correctly."""
    assert SigFigNum(num).value == expected

from decimal import Decimal

import pytest

# Addition and subtraction: preserve the least precise decimal place

@pytest.mark.parametrize(
    ("left", "right", "expected"),
    [
        ("12.34", "1.2", "13.5"),
        ("12.34", "1.23", "13.57"),
        ("100", "1", "100"),
        ("1500", "20", "1500"),
        ("1.25", "1.25", "2.50"),
    ],
)
def test_addition_precision(left, right, expected):
    result = SigFigNum(left) + SigFigNum(right)
    assert result.num_str == expected


@pytest.mark.parametrize(
    ("left", "right", "expected"),
    [
        ("12.34", "1.2", "11.1"),
        ("12.34", "1.23", "11.11"),
        ("5.50", "1.20", "4.30"),
        ("100", "1", "100"),
    ],
)
def test_subtraction_precision(left, right, expected):
    result = SigFigNum(left) - SigFigNum(right)
    assert result.num_str == expected


# Multiplication and division: preserve the fewest significant figures

@pytest.mark.parametrize(
    ("left", "right", "expected"),
    [
        ("12.34", "1.20", "14.8"),
        ("2.5", "2.0", "5.0"),
        ("3.00", "2.0", "6.0"),
        ("9.99", "1.0", "1.0e+1"),
        ("1.23", "2.00", "2.46"),
    ],
)
def test_multiplication_precision(left, right, expected):
    result = SigFigNum(left) * SigFigNum(right)
    assert result.num_str == expected


@pytest.mark.parametrize(
    ("left", "right", "expected"),
    [
        ("12.34", "1.20", "10.3"),
        ("1.0", "2.0", "0.50"),
        ("10.0", "4.0", "2.5"),
        ("1.00", "4.00", "0.250"),
        ("9.00", "2.00", "4.50"),
    ],
)
def test_division_precision(left, right, expected):
    result = SigFigNum(left) / SigFigNum(right)
    assert result.num_str == expected


# Rounding behavior

@pytest.mark.parametrize(
    ("value", "precision", "rounding_type", "expected"),
    [
        (Decimal("1.234"), 2, "decimals", "1.23"),
        (Decimal("1.235"), 2, "decimals", "1.24"),
        (Decimal("-1.235"), 2, "decimals", "-1.24"),
        (Decimal("12.5"), 0, "decimals", "13"),
        (Decimal(125), -1, "decimals", "130"),
        (Decimal("1.234"), 3, "sigfigs", "1.23"),
        (Decimal("1.235"), 3, "sigfigs", "1.24"),
        (Decimal("999.5"), 3, "sigfigs", "1.00e+3"),
        (Decimal("0.001234"), 3, "sigfigs", "0.00123"),
        (Decimal(12345), 3, "sigfigs", "1.23e+4"),
        (Decimal("-12.55"), 3, "sigfigs", "-12.6"),
    ],
)
def test_round_num_string(value, precision, rounding_type, expected):
    result = SigFigNum("1")._round_num_string(
        value, precision, rounding_type
    )
    assert result == expected


def test_round_num_string_zero_significant_figures():
    with pytest.raises(ValueError):
        SigFigNum("1")._round_num_string(
            Decimal("1.23"), 0, "sigfigs"
        )


def test_round_num_string_invalid_type():
    with pytest.raises(ValueError):
        SigFigNum("1")._round_num_string(
            Decimal("1.23"), 2, "invalid"
        )


# Zero handling

@pytest.mark.parametrize(
    ("value", "expected_sigfigs"),
    [
        ("0", 1),
        ("0.0", 2),
        ("0.00", 3),
        ("0.000", 4),
    ],
)
def test_zero_significant_figures(value, expected_sigfigs):
    assert SigFigNum(value).sigfigs == expected_sigfigs


def test_multiplication_by_zero():
    result = SigFigNum("0.00") * SigFigNum("2.0")
    assert result.value == Decimal(0)
    assert result.sigfigs == 2


# Scientific notation and exponent handling

@pytest.mark.parametrize(
    ("value", "expected_value"),
    [
        ("1.23e3", Decimal("1.23e3")),
        ("1.23e-3", Decimal("1.23e-3")),
        ("-4.50e2", Decimal("-4.50e2")),
        ("1e+6", Decimal("1e6")),
    ],
)
def test_scientific_notation_value(value, expected_value):
    assert SigFigNum(value).value == expected_value


def test_scientific_notation_precision():
    number = SigFigNum("1.23e3")
    assert number.sigfigs == 3
    assert number.decimals == -1


# Division by zero

def test_division_by_zero():
    with pytest.raises(ZeroDivisionError):
        SigFigNum("1.0") / SigFigNum("0") # type: ignore


# Unsupported operand types

@pytest.mark.parametrize(
    "operation",
    [
        lambda number: number + 1,
        lambda number: number - 1,
        lambda number: number * 2,
        lambda number: number / 2,
    ],
)
def test_arithmetic_with_unsupported_type(operation):
    with pytest.raises(TypeError):
        operation(SigFigNum("2.0"))


# Equality: these tests assume equality means identical numeric strings

def test_equal_identical_strings():
    assert SigFigNum("1.20") == SigFigNum("1.20")


def test_unequal_strings_with_different_precision():
    assert SigFigNum("1.0") != SigFigNum("1.00")


def test_unequal_numeric_values():
    assert SigFigNum("1.2") != SigFigNum("1.3")


def test_equality_with_unsupported_type():
    assert SigFigNum("1.0") != 1.0


# Invalid input

@pytest.mark.parametrize(
    "value",
    [
        "",
        "abc",
        "1.2.3",
        "1e",
        "--2",
        ".5",
    ],
)
def test_invalid_numeric_strings(value):
    with pytest.raises((ValueError, TypeError)):
        SigFigNum(value)
