import pytest
import cogiles
from cogiles.colors import rgb_to_symbol, symbol_to_color_info, COLOR_LIST


class TestColorRegistry:
    def test_all_colors_present(self):
        """COLORS dict has 17 canonical + Bl alias = 18 entries."""
        assert len(cogiles.COLORS) == 18

    @pytest.mark.parametrize(
        "symbol,expected_rgb",
        [
            ("R", (1.0, 0.0, 0.0)),
            ("G", (0.0, 0.5, 0.0)),
            ("B", (0.0, 0.0, 1.0)),
            ("Y", (1.0, 1.0, 0.0)),
            ("C", (0.0, 1.0, 1.0)),
            ("M", (1.0, 0.0, 1.0)),
            ("W", (1.0, 1.0, 1.0)),
            ("K", (0.0, 0.0, 0.0)),
            ("O", (1.0, 0.5, 0.0)),
            ("P", (0.5, 0.0, 0.5)),
            ("T", (0.0, 0.5, 0.5)),
            ("L", (0.5, 1.0, 0.0)),
            ("I", (0.3, 0.0, 0.5)),
        ],
    )
    def test_single_letter_symbols(self, symbol, expected_rgb):
        assert cogiles.COLORS[symbol]["rgb"] == expected_rgb

    @pytest.mark.parametrize(
        "symbol,expected_rgb",
        [
            ("Br", (0.6, 0.3, 0.0)),
            ("Gr", (0.5, 0.5, 0.5)),
            ("Pi", (1.0, 0.4, 0.7)),
            ("Ol", (0.5, 0.5, 0.0)),
        ],
    )
    def test_two_letter_symbols(self, symbol, expected_rgb):
        assert cogiles.COLORS[symbol]["rgb"] == expected_rgb

    def test_bl_alias_is_black(self):
        assert cogiles.COLORS["Bl"]["rgb"] == cogiles.COLORS["K"]["rgb"]
        assert cogiles.COLORS["Bl"]["name"] == "black"

    def test_color_names(self):
        assert cogiles.COLORS["R"]["name"] == "red"
        assert cogiles.COLORS["K"]["name"] == "black"
        assert cogiles.COLORS["Br"]["name"] == "brown"
        assert cogiles.COLORS["Ol"]["name"] == "olive"

    def test_color_list_has_17_entries(self):
        assert len(COLOR_LIST) == 17


class TestSymbolToColorInfo:
    def test_known_symbol(self):
        info = symbol_to_color_info("R")
        assert info.symbol == "R"
        assert info.name == "red"
        assert info.rgb == (1.0, 0.0, 0.0)

    def test_two_letter_symbol(self):
        info = symbol_to_color_info("Br")
        assert info.symbol == "Br"
        assert info.name == "brown"

    def test_bl_alias_returns_canonical_k(self):
        info = symbol_to_color_info("Bl")
        assert info.symbol == "K"
        assert info.name == "black"

    def test_unknown_symbol_raises(self):
        with pytest.raises(KeyError):
            symbol_to_color_info("X")


class TestRgbToSymbol:
    @pytest.mark.parametrize(
        "rgb,expected_symbol",
        [
            ((1.0, 0.0, 0.0), "R"),
            ((0.0, 0.5, 0.0), "G"),
            ((0.0, 0.0, 1.0), "B"),
            ((1.0, 1.0, 0.0), "Y"),
            ((0.0, 1.0, 1.0), "C"),
            ((1.0, 0.0, 1.0), "M"),
            ((1.0, 1.0, 1.0), "W"),
            ((0.0, 0.0, 0.0), "K"),
            ((1.0, 0.5, 0.0), "O"),
            ((0.5, 0.0, 0.5), "P"),
            ((0.0, 0.5, 0.5), "T"),
            ((0.5, 1.0, 0.0), "L"),
            ((0.3, 0.0, 0.5), "I"),
            ((0.6, 0.3, 0.0), "Br"),
            ((0.5, 0.5, 0.5), "Gr"),
            ((1.0, 0.4, 0.7), "Pi"),
            ((0.5, 0.5, 0.0), "Ol"),
        ],
    )
    def test_exact_match(self, rgb, expected_symbol):
        assert rgb_to_symbol(rgb) == expected_symbol

    @pytest.mark.parametrize(
        "rgb,expected_symbol",
        [
            ((0.9, 0.1, 0.1), "R"),
            ((0.05, 0.45, 0.05), "G"),
            ((0.55, 0.55, 0.55), "Gr"),
            ((0.42, 0.42, 0.42), "Gr"),
        ],
    )
    def test_nearest_match_default(self, rgb, expected_symbol):
        assert rgb_to_symbol(rgb) == expected_symbol

    def test_strict_mode_exact(self):
        assert rgb_to_symbol((1.0, 0.0, 0.0), strict=True) == "R"

    def test_strict_mode_raises(self):
        with pytest.raises(cogiles.CogilesEncodeError):
            rgb_to_symbol((0.9, 0.1, 0.1), strict=True)

    def test_floating_point_tolerance(self):
        """Values within 4-decimal rounding are treated as exact matches."""
        assert rgb_to_symbol((0.99999, 0.00001, 0.00001)) == "R"

    def test_floating_point_tolerance_strict(self):
        """4-decimal rounding also applies in strict mode."""
        assert rgb_to_symbol((0.99999, 0.00001, 0.00001), strict=True) == "R"

    def test_strict_mode_error_has_attributes(self):
        with pytest.raises(cogiles.CogilesEncodeError) as exc_info:
            rgb_to_symbol((0.9, 0.1, 0.1), strict=True, node=5)
        assert exc_info.value.color == (0.9, 0.1, 0.1)
        assert exc_info.value.node == 5

    def test_nearest_match_is_deterministic(self):
        """Tie-breaking is deterministic (first in COLOR_LIST wins)."""
        result1 = rgb_to_symbol((0.5, 0.5, 0.25))
        result2 = rgb_to_symbol((0.5, 0.5, 0.25))
        assert result1 == result2

    def test_nearest_match_returns_valid_symbol(self):
        """Any RGB input should produce a symbol that cogiles.parse accepts."""
        for rgb in [(0.1, 0.2, 0.3), (0.99, 0.01, 0.5), (0.0, 0.0, 0.0)]:
            sym = rgb_to_symbol(rgb)
            G = cogiles.parse(sym)
            assert G.number_of_nodes() == 1
