"""Tests for speaker diarization infrastructure.

Tests the SpeakerTurn DTO and speaker assignment utilities.
"""

from __future__ import annotations

import pytest

from noteflow.infrastructure.diarization import SpeakerTurn, assign_speaker, assign_speakers_batch

# Test constants for speaker turn timing
TURN_END_TIME_SHORT = 15.0
"""Short turn end time in seconds for testing confidence and turn assignment."""

TURN_END_TIME_MEDIUM = 12.0
"""Medium turn end time in seconds for testing partial overlaps."""

TURN_END_TIME_LONG = 20.0
"""Long turn end time in seconds for testing full overlaps and containment."""


class TestSpeakerTurn:
    """Tests for the SpeakerTurn dataclass."""

    @pytest.mark.parametrize(
        "attr,expected",
        [("speaker", "SPEAKER_00"), ("start", 0.0), ("end", 5.0), ("confidence", 1.0)],
    )
    def test_speaker_turn_attributes(self, attr: str, expected: object) -> None:
        """Test SpeakerTurn stores attributes correctly."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0)
        assert getattr(turn, attr) == expected, f"Expected {attr}={expected!r}, got {getattr(turn, attr)!r}"

    def test_create_turn_with_confidence(self) -> None:
        """Create a turn with custom confidence."""
        turn = SpeakerTurn(speaker="SPEAKER_01", start=10.0, end=TURN_END_TIME_SHORT, confidence=0.85)
        assert turn.confidence == 0.85, "Custom confidence should be stored correctly"

    def test_invalid_end_before_start_raises(self) -> None:
        """End time before start time raises ValueError."""
        with pytest.raises(ValueError, match=r"end.*<.*start"):
            SpeakerTurn(speaker="SPEAKER_00", start=10.0, end=5.0)

    def test_invalid_confidence_negative_raises(self) -> None:
        """Negative confidence raises ValueError."""
        with pytest.raises(ValueError, match=r"Confidence must be 0\.0-1\.0"):
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0, confidence=-0.1)

    def test_invalid_confidence_above_one_raises(self) -> None:
        """Confidence above 1.0 raises ValueError."""
        with pytest.raises(ValueError, match=r"Confidence must be 0\.0-1\.0"):
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0, confidence=1.5)

    def test_speaker_turn_duration_property(self) -> None:
        """Duration property calculates correctly."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=2.5, end=7.5)
        assert turn.duration == 5.0, "Duration should be end - start (7.5 - 2.5 = 5.0)"

    @pytest.mark.parametrize(
        "start,end", [(3.0, 7.0), (7.0, 12.0), (5.0, 10.0), (0.0, 15.0)]
    )
    def test_overlaps_returns_true(self, start: float, end: float) -> None:
        """overlaps() returns True when ranges overlap."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
        assert turn.overlaps(start, end), f"Turn [5.0, 10.0] should overlap with [{start}, {end}]"

    @pytest.mark.parametrize(
        "start,end", [(0.0, 5.0), (10.0, TURN_END_TIME_SHORT), (0.0, 3.0), (TURN_END_TIME_MEDIUM, TURN_END_TIME_LONG)]
    )
    def test_overlaps_returns_false(self, start: float, end: float) -> None:
        """overlaps() returns False when ranges don't overlap."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
        assert not turn.overlaps(start, end), f"Turn [5.0, 10.0] should not overlap with [{start}, {end}]"

    def test_overlap_duration_full_overlap(self) -> None:
        """overlap_duration() for full overlap returns turn duration."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
        assert turn.overlap_duration(0.0, TURN_END_TIME_SHORT) == 5.0, "Full overlap should return entire turn duration"

    def test_overlap_duration_partial_overlap_left(self) -> None:
        """overlap_duration() for partial overlap on left side."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
        assert turn.overlap_duration(3.0, 7.0) == 2.0, "Left partial overlap [3.0, 7.0] with [5.0, 10.0] should be 2.0"

    def test_overlap_duration_partial_overlap_right(self) -> None:
        """overlap_duration() for partial overlap on right side."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
        assert turn.overlap_duration(8.0, TURN_END_TIME_SHORT) == 2.0, "Right partial overlap [8.0, 15.0] with [5.0, 10.0] should be 2.0"

    def test_overlap_duration_contained(self) -> None:
        """overlap_duration() when range is contained within turn."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=TURN_END_TIME_LONG)
        assert turn.overlap_duration(5.0, 10.0) == 5.0, "Contained range [5.0, 10.0] within [0.0, 20.0] should return range duration"

    def test_overlap_duration_no_overlap(self) -> None:
        """overlap_duration() returns 0.0 when no overlap."""
        turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
        assert turn.overlap_duration(0.0, 3.0) == 0.0, "No overlap before turn should return 0.0"
        assert turn.overlap_duration(TURN_END_TIME_MEDIUM, TURN_END_TIME_LONG) == 0.0, "No overlap after turn should return 0.0"


class TestAssignSpeaker:
    """Tests for the assign_speaker function."""

    def test_empty_turns_returns_none(self) -> None:
        """Empty turns list returns None with 0 confidence."""
        speaker, confidence = assign_speaker(0.0, 5.0, [])
        assert speaker is None, "Empty turns should return None speaker"
        assert confidence == 0.0, "Empty turns should return 0.0 confidence"

    def test_zero_duration_segment_returns_none(self) -> None:
        """Zero duration segment returns None."""
        turns = [SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=10.0)]
        speaker, confidence = assign_speaker(5.0, 5.0, turns)
        assert speaker is None, "Zero duration segment should return None speaker"
        assert confidence == 0.0, "Zero duration segment should return 0.0 confidence"

    def test_negative_duration_segment_returns_none(self) -> None:
        """Negative duration segment (end < start) returns None."""
        turns = [SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=10.0)]
        speaker, confidence = assign_speaker(8.0, 3.0, turns)
        assert speaker is None, "Negative duration segment should return None speaker"
        assert confidence == 0.0, "Negative duration segment should return 0.0 confidence"

    def test_single_turn_full_overlap(self) -> None:
        """Single turn with full overlap returns high confidence."""
        turns = [SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=10.0)]
        speaker, confidence = assign_speaker(2.0, 8.0, turns)
        assert speaker == "SPEAKER_00", "Full overlap should return the speaker"
        assert confidence == 1.0, "Full overlap should return 1.0 confidence"

    def test_single_turn_partial_overlap(self) -> None:
        """Single turn with partial overlap returns proportional confidence."""
        turns = [SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)]
        speaker, confidence = assign_speaker(0.0, 10.0, turns)
        assert speaker == "SPEAKER_00", "Partial overlap should return the speaker"
        assert confidence == 0.5, "50% overlap should return 0.5 confidence"

    def test_multiple_turns_chooses_dominant_speaker(self) -> None:
        """Multiple turns chooses speaker with most overlap."""
        turns = [
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=3.0),
            SpeakerTurn(speaker="SPEAKER_01", start=3.0, end=10.0),
        ]
        speaker, confidence = assign_speaker(0.0, 10.0, turns)
        assert speaker == "SPEAKER_01", "Should choose speaker with most overlap (7s vs 3s)"
        assert confidence == 0.7, "Dominant speaker with 7/10s overlap should have 0.7 confidence"

    def test_no_turn_overlap_returns_none_speaker(self) -> None:
        """No overlapping speaker turns returns None speaker with zero confidence."""
        turns = [
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0),
            SpeakerTurn(speaker="SPEAKER_01", start=10.0, end=TURN_END_TIME_SHORT),
        ]
        speaker, confidence = assign_speaker(6.0, 9.0, turns)
        assert speaker is None, "No overlapping turns should return None speaker"
        assert confidence == 0.0, "No overlapping turns should return 0.0 confidence"

    def test_equal_overlap_chooses_first_encountered(self) -> None:
        """Equal overlap chooses first speaker encountered."""
        turns = [
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0),
            SpeakerTurn(speaker="SPEAKER_01", start=5.0, end=10.0),
        ]
        speaker, confidence = assign_speaker(3.0, 7.0, turns)
        # SPEAKER_00: overlap 2.0, SPEAKER_01: overlap 2.0
        # First one wins since > not >=
        assert speaker == "SPEAKER_00", "Equal overlap should choose first speaker encountered"
        assert confidence == 0.5, "Equal 2s overlap in 4s segment should return 0.5 confidence"


class TestAssignSpeakersBatch:
    """Tests for the assign_speakers_batch function."""

    def test_empty_segments(self) -> None:
        """Empty segments list returns empty results."""
        turns = [SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=10.0)]
        results = assign_speakers_batch([], turns)
        assert results == [], "Empty segments should return empty results list"

    def test_empty_turns(self) -> None:
        """Empty turns returns all None speakers."""
        segments = [(0.0, 5.0), (5.0, 10.0)]
        results = assign_speakers_batch(segments, [])
        assert len(results) == 2, "Should return result for each segment"
        assert all(speaker is None for speaker, _ in results), "Empty turns should assign None to all segments"
        assert all(conf == 0.0 for _, conf in results), "Empty turns should return 0.0 confidence for all segments"

    @pytest.mark.parametrize(
        "idx,expected",
        [(0, ("SPEAKER_00", 1.0)), (1, ("SPEAKER_01", 1.0)), (2, ("SPEAKER_00", 1.0))],
    )
    def test_batch_assignment(self, idx: int, expected: tuple[str, float]) -> None:
        """Batch assignment processes all segments."""
        turns = [
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0),
            SpeakerTurn(speaker="SPEAKER_01", start=5.0, end=10.0),
            SpeakerTurn(speaker="SPEAKER_00", start=10.0, end=15.0),
        ]
        segments = [(0.0, 5.0), (5.0, 10.0), (10.0, 15.0)]
        results = assign_speakers_batch(segments, turns)
        assert results[idx] == expected, f"Segment {idx} should be assigned {expected}"

    def test_batch_assignment_count(self) -> None:
        """Batch assignment returns correct count."""
        turns = [
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0),
            SpeakerTurn(speaker="SPEAKER_01", start=5.0, end=10.0),
            SpeakerTurn(speaker="SPEAKER_00", start=10.0, end=15.0),
        ]
        segments = [(0.0, 5.0), (5.0, 10.0), (10.0, 15.0)]
        results = assign_speakers_batch(segments, turns)
        assert len(results) == 3, "Should return result for each of 3 segments"

    def test_batch_with_gaps(self) -> None:
        """Batch assignment handles gaps between turns."""
        turns = [
            SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=3.0),
            SpeakerTurn(speaker="SPEAKER_01", start=7.0, end=10.0),
        ]
        segments = [(0.0, 3.0), (3.0, 7.0), (7.0, 10.0)]
        results = assign_speakers_batch(segments, turns)
        assert results[0] == ("SPEAKER_00", 1.0), "First segment should match SPEAKER_00"
        assert results[1] == (None, 0.0), "Gap segment should have no speaker assignment"
        assert results[2] == ("SPEAKER_01", 1.0), "Third segment should match SPEAKER_01"
