import pytest
import networkx as nx
import cogiles
from cogiles.fingerprint import FingerprintGenerator


class TestFingerprintBasic:
    def test_single_node_length(self):
        G = cogiles.parse("R")
        fp = cogiles.fingerprint(G)
        assert len(fp) == 2048
        assert sum(fp) > 0

    def test_all_bits_are_zero_or_one(self):
        G = cogiles.parse("RGBR")
        fp = cogiles.fingerprint(G)
        assert all(b in (0, 1) for b in fp)

    def test_empty_graph_returns_all_zeros(self):
        G = nx.Graph()
        fp = cogiles.fingerprint(G)
        assert len(fp) == 2048
        assert sum(fp) == 0


class TestFingerprintDeterminism:
    def test_same_graph_same_fingerprint(self):
        G = cogiles.parse("R-1RRRRR-1")
        fp1 = cogiles.fingerprint(G)
        fp2 = cogiles.fingerprint(G)
        assert fp1 == fp2

    def test_isomorphic_graphs_same_fingerprint(self):
        """Graphs with identical structure but different node IDs."""
        G1 = cogiles.parse("RGB")
        # Build the same chain manually with different node IDs.
        G2 = nx.Graph()
        G2.add_node(10, color=(1.0, 0.0, 0.0), symbol="R")
        G2.add_node(20, color=(0.0, 0.5, 0.0), symbol="G")
        G2.add_node(30, color=(0.0, 0.0, 1.0), symbol="B")
        G2.add_edge(10, 20)
        G2.add_edge(20, 30)
        fp1 = cogiles.fingerprint(G1)
        fp2 = cogiles.fingerprint(G2)
        assert fp1 == fp2

    def test_parse_encode_parse_same_fingerprint(self):
        """Round-tripping through encode/parse preserves the fingerprint."""
        G = cogiles.parse("R(G)(B)RR")
        fp1 = cogiles.fingerprint(G)
        s = cogiles.encode(G)
        G2 = cogiles.parse(s)
        fp2 = cogiles.fingerprint(G2)
        assert fp1 == fp2


class TestFingerprintDistinction:
    def test_different_graphs_different_fingerprints(self):
        fp1 = cogiles.fingerprint(cogiles.parse("RRR"))
        fp2 = cogiles.fingerprint(cogiles.parse("BBB"))
        assert fp1 != fp2

    def test_different_topology_different_fingerprints(self):
        """Chain vs cycle of same colors should differ."""
        chain = cogiles.parse("RRRR")
        cycle = cogiles.parse("R-1RRR-1")
        fp_chain = cogiles.fingerprint(chain)
        fp_cycle = cogiles.fingerprint(cycle)
        assert fp_chain != fp_cycle

    def test_different_radii_different_fingerprints(self):
        G = cogiles.parse("R-1RRRRR-1")
        fp_r1 = cogiles.fingerprint(G, radius=1)
        fp_r3 = cogiles.fingerprint(G, radius=3)
        assert fp_r1 != fp_r3


class TestFingerprintParameters:
    def test_custom_nbits(self):
        G = cogiles.parse("RGB")
        fp = cogiles.fingerprint(G, nbits=512)
        assert len(fp) == 512

    def test_custom_radius(self):
        G = cogiles.parse("RGB")
        fp = cogiles.fingerprint(G, radius=0)
        assert len(fp) == 2048
        assert sum(fp) > 0

    def test_radius_zero_only_captures_node_colors(self):
        """At radius 0, only node symbols matter — topology is ignored."""
        chain = cogiles.parse("RGB")
        # Same colors, different topology (star).
        star = cogiles.parse("R(G)(B)")
        fp_chain = cogiles.fingerprint(chain, radius=0)
        fp_star = cogiles.fingerprint(star, radius=0)
        assert fp_chain == fp_star

    def test_radius_one_distinguishes_topology(self):
        """At radius >= 1, topology matters."""
        chain = cogiles.parse("RGB")
        star = cogiles.parse("R(G)(B)")
        fp_chain = cogiles.fingerprint(chain, radius=1)
        fp_star = cogiles.fingerprint(star, radius=1)
        assert fp_chain != fp_star


class TestFingerprintDisconnected:
    def test_disconnected_graph(self):
        G = cogiles.parse("RR.BB")
        fp = cogiles.fingerprint(G)
        assert len(fp) == 2048
        assert sum(fp) > 0


class TestFingerprintGenerator:
    def test_generator_reusable(self):
        gen = FingerprintGenerator(radius=2, nbits=1024)
        G1 = cogiles.parse("RGR")
        G2 = cogiles.parse("BYB")
        fp1 = gen.fingerprint(G1)
        fp2 = gen.fingerprint(G2)
        assert len(fp1) == 1024
        assert len(fp2) == 1024
        assert fp1 != fp2

    def test_generator_matches_convenience_function(self):
        gen = FingerprintGenerator(radius=2, nbits=2048)
        G = cogiles.parse("R(G)(B)RR")
        assert gen.fingerprint(G) == cogiles.fingerprint(G)

    def test_generator_repr(self):
        gen = FingerprintGenerator(radius=3, nbits=512)
        assert repr(gen) == "FingerprintGenerator(radius=3, nbits=512)"

    def test_invalid_radius_raises(self):
        with pytest.raises(ValueError, match="radius"):
            FingerprintGenerator(radius=-1)

    def test_invalid_nbits_raises(self):
        with pytest.raises(ValueError, match="nbits"):
            FingerprintGenerator(nbits=0)


class TestFingerprintColorFallback:
    def test_node_with_only_color_attribute(self):
        """Nodes without symbol should still work via color matching."""
        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)
        fp = cogiles.fingerprint(G)
        assert len(fp) == 2048
        assert sum(fp) > 0

    def test_no_color_no_symbol_raises(self):
        G = nx.Graph()
        G.add_node(0)
        with pytest.raises(ValueError):
            cogiles.fingerprint(G)


class TestJaccardSimilarityBasic:
    def test_identical_graphs_return_one(self):
        G = cogiles.parse("R-1RRRRR-1")
        assert cogiles.jaccard_similarity(G, G) == 1.0

    def test_completely_different_graphs(self):
        sim = cogiles.jaccard_similarity(
            cogiles.parse("RRR"), cogiles.parse("BBB")
        )
        assert 0.0 <= sim < 1.0

    def test_both_empty_returns_one(self):
        assert cogiles.jaccard_similarity(nx.Graph(), nx.Graph()) == 1.0

    def test_result_in_zero_one_range(self):
        sim = cogiles.jaccard_similarity(
            cogiles.parse("RGBR"), cogiles.parse("RRRR")
        )
        assert 0.0 <= sim <= 1.0


class TestJaccardSimilarityInputTypes:
    def test_two_cogiles_strings(self):
        sim = cogiles.jaccard_similarity("RGR", "RGR")
        assert sim == 1.0

    def test_two_graphs(self):
        G1 = cogiles.parse("RGB")
        G2 = cogiles.parse("RGB")
        assert cogiles.jaccard_similarity(G1, G2) == 1.0

    def test_string_and_graph_mixed(self):
        G = cogiles.parse("RGB")
        sim = cogiles.jaccard_similarity("RGB", G)
        assert sim == 1.0

    def test_precomputed_fingerprints(self):
        fp1 = cogiles.fingerprint(cogiles.parse("RGB"))
        fp2 = cogiles.fingerprint(cogiles.parse("RGB"))
        assert cogiles.jaccard_similarity(fp1, fp2) == 1.0

    def test_fingerprint_and_string_mixed(self):
        fp = cogiles.fingerprint(cogiles.parse("RGB"))
        sim = cogiles.jaccard_similarity(fp, "RGB")
        assert sim == 1.0

    def test_invalid_type_raises(self):
        with pytest.raises(TypeError):
            cogiles.jaccard_similarity(42, "RGB")


class TestJaccardSimilarityParameters:
    def test_custom_radius(self):
        sim_r1 = cogiles.jaccard_similarity("R-1RRRRR-1", "RRRRRR", radius=1)
        sim_r3 = cogiles.jaccard_similarity("R-1RRRRR-1", "RRRRRR", radius=3)
        # Different radii should generally give different similarities
        assert isinstance(sim_r1, float)
        assert isinstance(sim_r3, float)

    def test_custom_nbits(self):
        sim = cogiles.jaccard_similarity("RGB", "RGR", nbits=512)
        assert 0.0 <= sim <= 1.0

    def test_mismatched_fingerprint_lengths_raises(self):
        fp1 = cogiles.fingerprint(cogiles.parse("RGB"), nbits=1024)
        fp2 = cogiles.fingerprint(cogiles.parse("RGB"), nbits=2048)
        with pytest.raises(ValueError, match="lengths differ"):
            cogiles.jaccard_similarity(fp1, fp2)


class TestJaccardSimilaritySymmetry:
    def test_symmetric(self):
        sim_ab = cogiles.jaccard_similarity("RGBR", "RRRR")
        sim_ba = cogiles.jaccard_similarity("RRRR", "RGBR")
        assert sim_ab == sim_ba

    def test_similar_graphs_higher_than_different(self):
        """Graphs sharing structure should be more similar than unrelated ones."""
        base = "R(G)(B)RR"
        similar = "R(G)(B)RG"  # one color changed
        different = "YYYYY"
        sim_close = cogiles.jaccard_similarity(base, similar)
        sim_far = cogiles.jaccard_similarity(base, different)
        assert sim_close > sim_far
