import pytest
import sys

sys.path.insert(0, "src")

from src.pitch import Pitch, DIMS_4, DIMS_7
from src.chord import Chord
from src.harmonic_space import HarmonicSpace


class TestPitch:
    """Tests for Pitch class."""

    def test_fundamental(self):
        p = Pitch((0, 0, 0, 0), DIMS_4)
        assert p.to_fraction() == 1

    def test_octave(self):
        p = Pitch((1, 0, 0, 0), DIMS_4)
        assert p.to_fraction() == 2

    def test_fifth(self):
        p = Pitch((0, 1, 0, 0), DIMS_4)
        assert p.to_fraction() == 3

    def test_compound(self):
        p = Pitch((1, 1, 0, 0), DIMS_4)
        assert p.to_fraction() == 6

    def test_cents_fundamental(self):
        p = Pitch((0, 0, 0, 0), DIMS_4)
        assert p.to_cents() == 0.0

    def test_cents_octave(self):
        p = Pitch((1, 0, 0, 0), DIMS_4)
        assert p.to_cents() == 1200.0

    def test_cents_fifth(self):
        p = Pitch((0, 1, 0, 0), DIMS_4)
        assert abs(p.to_cents() - 1901.96) < 0.01

    def test_transpose_positive(self):
        p = Pitch((0, 1, 0, 0), DIMS_4)
        trans = Pitch((1, 0, 0, 0), DIMS_4)
        result = p.transpose(trans)
        assert result.hs_array == (1, 1, 0, 0)

    def test_transpose_negative(self):
        p = Pitch((1, 1, 0, 0), DIMS_4)
        trans = Pitch((-1, 0, 0, 0), DIMS_4)
        result = p.transpose(trans)
        assert result.hs_array == (0, 1, 0, 0)

    def test_pitch_difference(self):
        p1 = Pitch((0, 1, 0, 0), DIMS_4)
        p2 = Pitch((0, 0, 0, 0), DIMS_4)
        result = p1.pitch_difference(p2)
        assert result.hs_array == (0, 1, 0, 0)

    def test_collapse(self):
        p = Pitch((3, 1, 0, 0), DIMS_4)
        collapsed = p.collapse()
        # Collapse brings to [1, 2) range: 24 -> 1.5 = 2^-1 * 3
        assert collapsed.hs_array == (-1, 1, 0, 0)

    def test_collapse_below_one(self):
        p = Pitch((-2, 0, 0, 0), DIMS_4)
        collapsed = p.collapse()
        assert collapsed.hs_array == (0, 0, 0, 0)

    def test_hash(self):
        p1 = Pitch((0, 1, 0, 0), DIMS_4)
        p2 = Pitch((0, 1, 0, 0), DIMS_4)
        assert hash(p1) == hash(p2)

    def test_equality(self):
        p1 = Pitch((0, 1, 0, 0), DIMS_4)
        p2 = Pitch((0, 1, 0, 0), DIMS_4)
        assert p1 == p2

    def test_len(self):
        p = Pitch((0, 1, 0, 0), DIMS_4)
        assert len(p) == 4


class TestChord:
    """Tests for Chord class."""

    def test_create_chord(self):
        p1 = Pitch((0, 0, 0, 0), DIMS_4)
        p2 = Pitch((0, 1, 0, 0), DIMS_4)
        chord = Chord((p1, p2), DIMS_4)
        assert len(chord) == 2

    def test_transpose(self):
        p1 = Pitch((0, 0, 0, 0), DIMS_4)
        p2 = Pitch((0, 1, 0, 0), DIMS_4)
        chord = Chord((p1, p2), DIMS_4)
        trans = Pitch((1, 0, 0, 0), DIMS_4)
        transposed = chord.transpose(trans)
        assert transposed[0].hs_array == (1, 0, 0, 0)
        assert transposed[1].hs_array == (1, 1, 0, 0)

    def test_is_connected_single(self):
        p = Pitch((0, 0, 0, 0), DIMS_4)
        chord = Chord((p,), DIMS_4)
        assert chord.is_connected() is True

    def test_is_connected_adjacent(self):
        p1 = Pitch((0, 0, 0, 0), DIMS_4)
        p2 = Pitch((0, 1, 0, 0), DIMS_4)
        chord = Chord((p1, p2), DIMS_4)
        assert chord.is_connected() is True

    def test_is_connected_not_adjacent(self):
        p1 = Pitch((0, 0, 0, 0), DIMS_4)
        p2 = Pitch((0, 2, 0, 0), DIMS_4)
        chord = Chord((p1, p2), DIMS_4)
        assert chord.is_connected() is False

    def test_sorted_by_frequency(self):
        p1 = Pitch((0, 1, 0, 0), DIMS_4)  # 3/2
        p2 = Pitch((0, 0, 0, 0), DIMS_4)  # 1/1
        chord = Chord((p1, p2), DIMS_4)
        sorted_pitches = chord.sorted_by_frequency()
        assert sorted_pitches[0].hs_array == (0, 0, 0, 0)
        assert sorted_pitches[1].hs_array == (0, 1, 0, 0)

    def test_symmetric_difference_size(self):
        p1 = Pitch((0, 0, 0, 0), DIMS_4)
        p2 = Pitch((0, 1, 0, 0), DIMS_4)
        p3 = Pitch((0, 2, 0, 0), DIMS_4)
        chord1 = Chord((p1, p2), DIMS_4)
        chord2 = Chord((p1, p3), DIMS_4)
        # Both have p1, differ on p2 vs p3: symdiff = 2
        assert chord1.symmetric_difference_size(chord2) == 2

    def test_hash(self):
        p1 = Pitch((0, 0, 0, 0), DIMS_4)
        p2 = Pitch((0, 1, 0, 0), DIMS_4)
        c1 = Chord((p1, p2), DIMS_4)
        c2 = Chord((p1, p2), DIMS_4)
        assert hash(c1) == hash(c2)


class TestHarmonicSpace:
    """Tests for HarmonicSpace class."""

    def test_create_space(self):
        space = HarmonicSpace(DIMS_4)
        assert space.dims == DIMS_4
        assert space.collapsed is True

    def test_root(self):
        space = HarmonicSpace(DIMS_4)
        root = space.root()
        assert root.hs_array == (0, 0, 0, 0)

    def test_generate_connected_sets_size_3(self):
        space = HarmonicSpace(DIMS_4)
        chords = space.generate_connected_sets(3, 3)
        assert len(chords) > 0
        assert all(len(c) == 3 for c in chords)

    def test_generate_connected_sets_all_connected(self):
        space = HarmonicSpace(DIMS_4)
        chords = space.generate_connected_sets(3, 3)
        assert all(c.is_connected() for c in chords)

    def test_generate_connected_sets_size_2(self):
        space = HarmonicSpace(DIMS_4)
        chords = space.generate_connected_sets(2, 2)
        assert len(chords) > 0
        assert all(len(c) == 2 for c in chords)

    def test_build_voice_leading_graph(self):
        space = HarmonicSpace(DIMS_4)
        chords = space.generate_connected_sets(3, 3)
        graph = space.build_voice_leading_graph(chords, 2, 2)
        assert graph.number_of_nodes() == len(chords)
        assert graph.number_of_edges() > 0

    def test_build_graph_symdiff_filter(self):
        space = HarmonicSpace(DIMS_4)
        chords = space.generate_connected_sets(3, 3)
        # Test with symdiff range 1-3
        graph = space.build_voice_leading_graph(chords, 1, 3)
        assert graph.number_of_edges() > 0


class TestCLI:
    """Tests for CLI functionality."""

    def test_args_parsing(self):
        import argparse
        from src.io import main

        # This is a basic test - in practice we'd test the parser more thoroughly
        # For now just verify the module can be imported
        assert main is not None

    def test_target_register_weight(self):
        """Test that target register weight can be enabled and influences path generation."""
        from src.harmonic_space import HarmonicSpace
        from src.pathfinder import PathFinder

        space = HarmonicSpace(DIMS_4)
        chords = space.generate_connected_sets(3, 3)
        graph = space.build_voice_leading_graph(chords, 2, 2)

        path_finder = PathFinder(graph)

        # Test with target register disabled (default)
        config_no_target = path_finder._default_weights_config()
        assert config_no_target.get("target_register") is False

        # Test with target register enabled
        config_target = path_finder._default_weights_config()
        config_target["target_register"] = True
        config_target["target_register_octaves"] = 2.0

        assert config_target.get("target_register") is True
        assert config_target.get("target_register_octaves") == 2.0


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
