import warnings

import pytest
import networkx as nx
import cogiles


class TestEncodeSimple:
    def test_single_node(self):
        G = cogiles.parse("R")
        assert cogiles.encode(G) == "R"

    def test_chain(self):
        G = cogiles.parse("RGR")
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 3
        assert G2.number_of_edges() == 2
        colors = sorted(G2.nodes[n]["symbol"] for n in G2.nodes())
        assert colors == ["G", "R", "R"]

    def test_empty_graph(self):
        G = nx.Graph()
        assert cogiles.encode(G) == ""

    def test_two_letter_symbols_encoded(self):
        G = cogiles.parse("BrGrPi")
        s = cogiles.encode(G)
        assert "Br" in s
        assert "Gr" in s
        assert "Pi" in s

    def test_single_two_letter_node(self):
        G = cogiles.parse("Ol")
        assert cogiles.encode(G) == "Ol"


class TestEncodeCycles:
    def test_simple_cycle(self):
        G = cogiles.parse("R-1RRRRR-1")
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 6
        assert G2.number_of_edges() == 6
        assert all(d == 2 for _, d in G2.degree())

    def test_five_node_cycle(self):
        G = cogiles.parse("R-1RRRR-1")
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 5
        assert G2.number_of_edges() == 5


class TestEncodeDisconnected:
    def test_two_components(self):
        G = cogiles.parse("RR.BB")
        s = cogiles.encode(G)
        assert "." in s
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 4
        components = list(nx.connected_components(G2))
        assert len(components) == 2

    def test_three_isolates(self):
        G = cogiles.parse("R.G.B")
        s = cogiles.encode(G)
        assert s.count(".") == 2


class TestEncodeBranches:
    def test_star_pattern(self):
        G = cogiles.parse("Y(G)(G)(G)")
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 4
        assert G2.number_of_edges() == 3

    def test_branch_with_continuation(self):
        G = cogiles.parse("RR(BB)RR")
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 6
        assert G2.number_of_edges() == 5


class TestEncodeStrictMode:
    def test_strict_with_known_colors(self):
        G = cogiles.parse("RGB")
        s = cogiles.encode(G, strict=True)
        assert isinstance(s, str)

    def test_strict_with_unknown_color_raises(self):
        G = nx.Graph()
        G.add_node(0, color=(0.123, 0.456, 0.789))
        with pytest.raises(cogiles.CogilesEncodeError) as exc_info:
            cogiles.encode(G, strict=True)
        assert exc_info.value.color == (0.123, 0.456, 0.789)
        assert exc_info.value.node == 0

    def test_lenient_with_unknown_color_succeeds(self):
        G = nx.Graph()
        G.add_node(0, color=(0.123, 0.456, 0.789))
        s = cogiles.encode(G)
        assert isinstance(s, str)
        assert len(s) > 0


class TestEncodeNodeMissingColor:
    def test_no_color_attribute_raises(self):
        G = nx.Graph()
        G.add_node(0)
        with pytest.raises(cogiles.CogilesEncodeError):
            cogiles.encode(G)


class TestEncodeColorOnly:
    def test_encode_node_without_symbol_attribute(self):
        """Graph with only color attribute (no symbol) should encode via rgb_to_symbol."""
        G = nx.Graph()
        G.add_node(0, color=(1.0, 0.0, 0.0))
        G.add_node(1, color=(0.0, 0.0, 1.0))
        G.add_edge(0, 1)
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 2
        assert G2.number_of_edges() == 1
        colors = sorted(G2.nodes[n]["symbol"] for n in G2.nodes())
        assert colors == ["B", "R"]

    def test_encode_nearest_color_without_symbol(self):
        """Unknown color without symbol falls back to nearest match."""
        G = nx.Graph()
        G.add_node(0, color=(0.95, 0.05, 0.05))
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.nodes[0]["symbol"] == "R"

    def test_encode_non_contiguous_node_ids(self):
        """Graph with non-contiguous node IDs should encode correctly."""
        G = nx.Graph()
        G.add_node(0, color=(1.0, 0.0, 0.0), symbol="R", color_name="red")
        G.add_node(5, color=(0.0, 0.0, 1.0), symbol="B", color_name="blue")
        G.add_edge(0, 5)
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 2
        assert G2.number_of_edges() == 1


class TestEncodeSelfLoops:
    def test_self_loop_ignored(self):
        G = nx.Graph()
        G.add_node(0, color=(1.0, 0.0, 0.0), symbol="R", color_name="red")
        G.add_node(1, color=(1.0, 0.0, 0.0), symbol="R", color_name="red")
        G.add_edge(0, 1)
        G.add_edge(0, 0)
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 2
        assert G2.number_of_edges() == 1


class TestEncodeConflictingAttributes:
    def test_symbol_preferred_over_color(self):
        """When symbol and color disagree, encoder uses the symbol attribute."""
        G = nx.Graph()
        G.add_node(0, color=(0.0, 0.0, 1.0), symbol="R", color_name="red")
        s = cogiles.encode(G)
        assert s == "R"

    def test_lenient_encode_produces_parseable_output(self):
        """Lenient encoding of any color should produce a parseable string."""
        G = nx.Graph()
        G.add_node(0, color=(0.123, 0.456, 0.789))
        G.add_node(1, color=(0.9, 0.8, 0.7))
        G.add_edge(0, 1)
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 2
        assert G2.number_of_edges() == 1

    def test_encode_string_node_ids(self):
        """Graphs with string node IDs should encode correctly."""
        G = nx.Graph()
        G.add_node("a", color=(1.0, 0.0, 0.0), symbol="R", color_name="red")
        G.add_node("b", color=(0.0, 0.0, 1.0), symbol="B", color_name="blue")
        G.add_edge("a", "b")
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 2
        assert G2.number_of_edges() == 1


class TestEncodeSymbolValidation:
    """Bug fix tests: encoder validates and canonicalizes symbol attribute."""

    def test_invalid_symbol_falls_back_to_color(self):
        """E-BUG-1: unrecognized symbol warns and falls back to rgb_to_symbol."""
        G = nx.Graph()
        G.add_node(0, color=(1.0, 0.0, 0.0), symbol="XX")
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            s = cogiles.encode(G)
            assert len(w) == 1
            assert "unrecognized symbol" in str(w[0].message).lower()
        G2 = cogiles.parse(s)
        assert G2.nodes[0]["symbol"] == "R"

    def test_empty_symbol_falls_back_to_color(self):
        """E-BUG-2: empty symbol warns and falls back to rgb_to_symbol."""
        G = nx.Graph()
        G.add_node(0, color=(0.0, 0.0, 1.0), symbol="")
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            s = cogiles.encode(G)
            assert len(w) == 1
        G2 = cogiles.parse(s)
        assert G2.nodes[0]["symbol"] == "B"

    def test_bl_alias_canonicalized_to_k(self):
        """E-BUG-3: Bl symbol encoded as canonical K for idempotency."""
        G = nx.Graph()
        G.add_node(0, color=(0.0, 0.0, 0.0), symbol="Bl")
        s = cogiles.encode(G)
        assert s == "K"

    def test_valid_symbol_used_without_color(self):
        """E-BUG-4: valid symbol works even when color is None."""
        G = nx.Graph()
        G.add_node(0, color=None, symbol="R")
        assert cogiles.encode(G) == "R"

    def test_valid_symbol_used_without_color_two_letter(self):
        """E-BUG-4: two-letter symbol works without color."""
        G = nx.Graph()
        G.add_node(0, color=None, symbol="Br")
        assert cogiles.encode(G) == "Br"

    def test_no_symbol_no_color_raises(self):
        """color=None with no symbol should still raise."""
        G = nx.Graph()
        G.add_node(0, color=None)
        with pytest.raises(cogiles.CogilesEncodeError):
            cogiles.encode(G)

    def test_invalid_symbol_no_color_raises(self):
        """Unrecognized symbol with color=None should raise."""
        G = nx.Graph()
        G.add_node(0, color=None, symbol="XX")
        with pytest.raises(cogiles.CogilesEncodeError):
            cogiles.encode(G)


class TestEncodeFromFixtures:
    def test_encode_simple_chain(self, simple_chain):
        s = cogiles.encode(simple_chain)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 3
        assert G2.number_of_edges() == 2

    def test_encode_simple_cycle(self, simple_cycle):
        s = cogiles.encode(simple_cycle)
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 6
        assert G2.number_of_edges() == 6

    def test_encode_disconnected(self, disconnected_graph):
        s = cogiles.encode(disconnected_graph)
        assert "." in s
        G2 = cogiles.parse(s)
        assert G2.number_of_nodes() == 4
