"""Unit tests for font transformation functions."""

import pytest

from armonia import fontfunctions as ff
from armonia.exceptions import FontNotFoundError
from armonia.theme import Theme
from armonia.typography import Font


def _theme_with(name: str, font: Font) -> Theme:
    theme = Theme()
    theme.set_font(name, font)
    return theme


class TestScaleSize:
    """Tests for the scale_size function."""

    def test_scale_size_increases(self) -> None:
        """Test that scale_size with factor > 1 increases the font size."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("large", ff.scale_size("body", 1.5))
        result = theme.get_font("large")
        assert abs(result.size - 24.0) < 0.001

    def test_scale_size_decreases(self) -> None:
        """Test that scale_size with factor < 1 decreases the font size."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("small", ff.scale_size("body", 0.5))
        result = theme.get_font("small")
        assert abs(result.size - 8.0) < 0.001

    def test_scale_size_preserves_other_properties(self) -> None:
        """Test that scale_size does not change family, weight, or italic."""
        theme = _theme_with("body", Font("Inter", 16.0, 700, True))
        theme.set_computed_font("scaled", ff.scale_size("body", 2.0))
        result = theme.get_font("scaled")
        assert result.family == "Inter"
        assert result.weight == 700
        assert result.italic is True

    @pytest.mark.parametrize("factor", [0.0, -0.5, -1.0])
    def test_scale_size_non_positive_factor_raises_at_factory(self, factor: float) -> None:
        """Test that a non-positive scale factor raises ValueError at factory call time."""
        with pytest.raises(ValueError, match="factor"):
            ff.scale_size("body", factor)


class TestAdjustSize:
    """Tests for the adjust_size function."""

    def test_adjust_size_positive_delta(self) -> None:
        """Test that a positive delta increases the font size."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("larger", ff.adjust_size("body", 4.0))
        result = theme.get_font("larger")
        assert abs(result.size - 20.0) < 0.001

    def test_adjust_size_negative_delta(self) -> None:
        """Test that a negative delta decreases the font size."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("smaller", ff.adjust_size("body", -4.0))
        result = theme.get_font("smaller")
        assert abs(result.size - 12.0) < 0.001

    def test_adjust_size_resulting_invalid_size_raises(self) -> None:
        """Test that a delta producing a non-positive size raises ValueError at compute time."""
        theme = _theme_with("body", Font("Inter", 4.0, 400, False))
        theme.set_computed_font("invalid", ff.adjust_size("body", -10.0))
        with pytest.raises(ValueError, match="size"):
            theme.get_font("invalid")


class TestBolder:
    """Tests for the bolder function."""

    def test_bolder_increases_weight(self) -> None:
        """Test that bolder increases the font weight."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("bold", ff.bolder("body", 300))
        result = theme.get_font("bold")
        assert result.weight == 700

    def test_bolder_default_amount(self) -> None:
        """Test that bolder uses 100 as the default amount."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("bold", ff.bolder("body"))
        result = theme.get_font("bold")
        assert result.weight == 500

    def test_bolder_clamps_at_1000(self) -> None:
        """Test that bolder clamps the weight at 1000."""
        theme = _theme_with("body", Font("Inter", 16.0, 900, False))
        theme.set_computed_font("bold", ff.bolder("body", 500))
        result = theme.get_font("bold")
        assert result.weight == 1000

    def test_bolder_preserves_other_properties(self) -> None:
        """Test that bolder does not change family, size, or italic."""
        theme = _theme_with("body", Font("Georgia", 18.0, 400, True))
        theme.set_computed_font("bold", ff.bolder("body"))
        result = theme.get_font("bold")
        assert result.family == "Georgia"
        assert result.size == 18.0
        assert result.italic is True


class TestLighter:
    """Tests for the lighter function."""

    def test_lighter_decreases_weight(self) -> None:
        """Test that lighter decreases the font weight."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("light", ff.lighter("body", 200))
        result = theme.get_font("light")
        assert result.weight == 200

    def test_lighter_default_amount(self) -> None:
        """Test that lighter uses 100 as the default amount."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("light", ff.lighter("body"))
        result = theme.get_font("light")
        assert result.weight == 300

    def test_lighter_clamps_at_1(self) -> None:
        """Test that lighter clamps the weight at 1."""
        theme = _theme_with("body", Font("Inter", 16.0, 100, False))
        theme.set_computed_font("thin", ff.lighter("body", 500))
        result = theme.get_font("thin")
        assert result.weight == 1


class TestItalic:
    """Tests for the italic function."""

    def test_italic_enables_italic(self) -> None:
        """Test that italic enables italic style."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("em", ff.italic("body"))
        result = theme.get_font("em")
        assert result.italic is True

    def test_italic_already_italic_stays_italic(self) -> None:
        """Test that italic is idempotent when font is already italic."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, True))
        theme.set_computed_font("em", ff.italic("body"))
        result = theme.get_font("em")
        assert result.italic is True

    def test_italic_preserves_other_properties(self) -> None:
        """Test that italic does not change family, size, or weight."""
        theme = _theme_with("body", Font("Roboto", 14.0, 700, False))
        theme.set_computed_font("em", ff.italic("body"))
        result = theme.get_font("em")
        assert result.family == "Roboto"
        assert result.size == 14.0
        assert result.weight == 700


class TestRoman:
    """Tests for the roman function."""

    def test_roman_disables_italic(self) -> None:
        """Test that roman disables italic style."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, True))
        theme.set_computed_font("upright", ff.roman("body"))
        result = theme.get_font("upright")
        assert result.italic is False

    def test_roman_already_roman_stays_roman(self) -> None:
        """Test that roman is idempotent when font is already roman."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("upright", ff.roman("body"))
        result = theme.get_font("upright")
        assert result.italic is False


class TestAdjust:
    """Tests for the adjust function."""

    def test_adjust_size_delta(self) -> None:
        """Test that adjust applies size_delta correctly."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("larger", ff.adjust("body", size_delta=8.0))
        result = theme.get_font("larger")
        assert abs(result.size - 24.0) < 0.001

    def test_adjust_weight_delta(self) -> None:
        """Test that adjust applies weight_delta correctly."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("bold", ff.adjust("body", weight_delta=300))
        result = theme.get_font("bold")
        assert result.weight == 700

    def test_adjust_make_italic(self) -> None:
        """Test that adjust applies make_italic override."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("em", ff.adjust("body", make_italic=True))
        result = theme.get_font("em")
        assert result.italic is True

    def test_adjust_make_italic_false_overrides(self) -> None:
        """Test that make_italic=False overrides an already-italic font."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, True))
        theme.set_computed_font("upright", ff.adjust("body", make_italic=False))
        result = theme.get_font("upright")
        assert result.italic is False

    def test_adjust_all_params(self) -> None:
        """Test that adjust applies all parameters simultaneously."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font(
            "heading",
            ff.adjust("body", size_delta=8.0, weight_delta=300, make_italic=True),
        )
        result = theme.get_font("heading")
        assert abs(result.size - 24.0) < 0.001
        assert result.weight == 700
        assert result.italic is True
        assert result.family == "Inter"

    def test_adjust_weight_clamping(self) -> None:
        """Test that adjust clamps weight within 1-1000."""
        theme = _theme_with("body", Font("Inter", 16.0, 900, False))
        theme.set_computed_font("maxed", ff.adjust("body", weight_delta=500))
        result = theme.get_font("maxed")
        assert result.weight == 1000

    def test_adjust_no_args_returns_equal_font(self) -> None:
        """Test that adjust with no deltas returns a font equal to the source."""
        theme = _theme_with("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("copy", ff.adjust("body"))
        source = theme.get_font("body")
        result = theme.get_font("copy")
        assert result == source


class TestAlias:
    """Tests for the alias function."""

    def test_alias_returns_same_font(self) -> None:
        """Test that alias returns the same Font value as the source."""
        font = Font("Inter", 16.0, 400, False)
        theme = _theme_with("body", font)
        theme.set_computed_font("ui", ff.alias("body"))
        result = theme.get_font("ui")
        assert result == font

    def test_alias_tracks_source_changes(self) -> None:
        """Test that alias resolves at compute time, not at factory time."""
        theme = Theme()
        theme.set_font("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("ui", ff.alias("body"))
        theme.set_font("body", Font("Roboto", 18.0, 500, False))
        result = theme.get_font("ui")
        assert result.family == "Roboto"
        assert result.size == 18.0

    def test_alias_nonexistent_font_raises_at_compute_time(self) -> None:
        """Test that alias to a nonexistent font raises FontNotFoundError at compute time."""
        theme = Theme()
        theme.set_computed_font("ui", ff.alias("nonexistent"))
        with pytest.raises(FontNotFoundError):
            theme.get_font("ui")


class TestComputedFontChaining:
    """Tests for chaining computed font functions."""

    def test_computed_font_can_derive_from_another_computed_font(self) -> None:
        """Test that a computed font can be based on another computed font."""
        theme = Theme()
        theme.set_font("body", Font("Inter", 16.0, 400, False))
        theme.set_computed_font("bold", ff.bolder("body", 300))
        theme.set_computed_font("bold_italic", ff.italic("bold"))
        result = theme.get_font("bold_italic")
        assert result.weight == 700
        assert result.italic is True
        assert result.family == "Inter"

    def test_computed_font_missing_base_raises(self) -> None:
        """Test that computing a font whose base font does not exist raises FontNotFoundError."""
        theme = Theme()
        theme.set_computed_font("heading", ff.scale_size("missing", 1.5))
        with pytest.raises(FontNotFoundError):
            theme.get_font("heading")
