"""
Tests for SSE stream parser.
"""

import json
from unittest.mock import MagicMock, Mock

import pytest

from getbiji_box.exceptions import GetBijiError
from getbiji_box.models import AudioSectionInfo, Note, ProgressItem
from getbiji_box.stream_parser import (
    StreamCallback,
    format_timestamp,
    handle_stream_response,
    parse_audio_note_sections,
    parse_link_note_progress,
    parse_sse_response,
    parse_timestamp,
)


class TestParseSSEResponse:
    """Test cases for parse_sse_response."""

    def _make_response(self, events, stream=True):
        """Helper to create a mock streaming response."""
        response = Mock()
        response.stream = stream

        # Build SSE text from events
        chunks = []
        for event in events:
            text = f"data: {json.dumps(event)}\n\n"
            chunks.append(text)

        response.iter_content = Mock(return_value=iter(chunks))
        return response

    def test_basic_event(self):
        events = [{"msg_type": -1, "data": {"note_id": "abc"}}]
        response = self._make_response(events)
        results = list(parse_sse_response(response))
        assert len(results) == 1
        assert results[0] == (-1, {"note_id": "abc"})

    def test_multiple_events(self):
        events = [
            {"msg_type": -1, "data": {"note_id": "abc"}},
            {"msg_type": 6, "data": [{"msg": "processing"}]},
            {"msg_type": -2, "data": {"id": "abc", "title": "Done"}},
        ]
        response = self._make_response(events)
        results = list(parse_sse_response(response))
        assert len(results) == 3
        assert results[0][0] == -1
        assert results[1][0] == 6
        assert results[2][0] == -2

    def test_no_stream_attribute(self):
        response = Mock(spec=[])
        response.stream = False
        with pytest.raises(GetBijiError, match="streaming"):
            list(parse_sse_response(response))

    def test_invalid_json_skipped(self):
        response = Mock()
        response.stream = True
        response.iter_content = Mock(return_value=iter(["data: {invalid json}\n\n"]))
        results = list(parse_sse_response(response))
        assert len(results) == 0

    def test_no_data_line(self):
        response = Mock()
        response.stream = True
        response.iter_content = Mock(return_value=iter(["event: keepalive\n\n"]))
        results = list(parse_sse_response(response))
        assert len(results) == 0

    def test_no_msg_type(self):
        response = Mock()
        response.stream = True
        response.iter_content = Mock(
            return_value=iter([f'data: {json.dumps({"data": "no type"})}\n\n'])
        )
        results = list(parse_sse_response(response))
        assert len(results) == 0

    def test_empty_data_field(self):
        response = Mock()
        response.stream = True
        response.iter_content = Mock(
            return_value=iter([f'data: {json.dumps({"msg_type": 6})}\n\n'])
        )
        results = list(parse_sse_response(response))
        assert len(results) == 1
        assert results[0] == (6, {})

    def test_chunked_event(self):
        """Test event split across multiple chunks."""
        response = Mock()
        response.stream = True
        event_text = f'data: {json.dumps({"msg_type": -1, "data": {"note_id": "x"}})}\n\n'
        # Split the event across two chunks
        mid = len(event_text) // 2
        response.iter_content = Mock(return_value=iter([event_text[:mid], event_text[mid:]]))
        results = list(parse_sse_response(response))
        assert len(results) == 1
        assert results[0][0] == -1


class TestParseLinkNoteProgress:
    """Test cases for parse_link_note_progress."""

    def test_list_format(self):
        data = [
            {"msg": "browsing", "processName": "browse", "processStatus": "end"},
            {"msg": "analyzing", "processName": "analyze", "processStatus": "start"},
        ]
        result = parse_link_note_progress(data)
        assert len(result) == 2
        assert result[0].msg == "browsing"
        assert result[0].process_name == "browse"
        assert result[1].process_status == "start"

    def test_string_format(self):
        result = parse_link_note_progress("some string data")
        assert result == []

    def test_empty_list(self):
        result = parse_link_note_progress([])
        assert result == []

    def test_non_dict_items(self):
        result = parse_link_note_progress(["string", 123])
        assert result == []

    def test_mixed_items(self):
        data = [{"msg": "ok", "processName": "p", "processStatus": "s"}, "skip"]
        result = parse_link_note_progress(data)
        assert len(result) == 1


class TestParseAudioNoteSections:
    """Test cases for parse_audio_note_sections."""

    def test_basic(self):
        content = (
            "Some intro text\n\n"
            "[00:05:23](https://getnotes.seek:323000) **Chapter One**\n\n"
            "Chapter one content\n\n"
            "[00:12:00](https://getnotes.seek:720000) **Chapter Two**\n\n"
        )
        sections = parse_audio_note_sections(content)
        assert len(sections) == 2
        assert sections[0].title == "Chapter One"
        assert sections[0].timestamp == "00:05:23"
        assert sections[0].milliseconds == 323000
        assert sections[1].title == "Chapter Two"
        assert sections[1].milliseconds == 720000

    def test_no_sections(self):
        content = "Just plain text without any chapters."
        sections = parse_audio_note_sections(content)
        assert sections == []

    def test_empty_content(self):
        sections = parse_audio_note_sections("")
        assert sections == []


class TestFormatTimestamp:
    """Test cases for format_timestamp."""

    def test_zero(self):
        assert format_timestamp(0) == "00:00:00"

    def test_seconds(self):
        assert format_timestamp(5000) == "00:00:05"

    def test_minutes(self):
        assert format_timestamp(125000) == "00:02:05"

    def test_hours(self):
        assert format_timestamp(3661000) == "01:01:01"


class TestParseTimestamp:
    """Test cases for parse_timestamp."""

    def test_basic(self):
        assert parse_timestamp("00:00:05") == 5000

    def test_minutes(self):
        assert parse_timestamp("00:02:05") == 125000

    def test_hours(self):
        assert parse_timestamp("01:01:01") == 3661000

    def test_invalid_format(self):
        assert parse_timestamp("invalid") == 0

    def test_two_parts(self):
        assert parse_timestamp("05:30") == 0


class TestStreamCallback:
    """Test cases for StreamCallback base class."""

    def test_on_init(self):
        cb = StreamCallback()
        cb.on_init("note123")  # should not raise

    def test_on_progress(self):
        cb = StreamCallback()
        cb.on_progress([])

    def test_on_chapter(self):
        cb = StreamCallback()
        info = AudioSectionInfo(title="Ch", description="", timestamp="00:00:00", milliseconds=0)
        cb.on_chapter(info)

    def test_on_complete(self):
        cb = StreamCallback()
        note = Note(id="n1", title="T", content="C")
        cb.on_complete(note)

    def test_on_error(self):
        cb = StreamCallback()
        cb.on_error("some error")


class TestHandleStreamResponse:
    """Test cases for handle_stream_response."""

    def _make_response(self, events):
        response = Mock()
        response.stream = True
        chunks = []
        for event in events:
            text = f"data: {json.dumps(event)}\n\n"
            chunks.append(text)
        response.iter_content = Mock(return_value=iter(chunks))
        return response

    def test_full_flow(self):
        events = [
            {"msg_type": -1, "data": {"note_id": "n1"}},
            {"msg_type": 6, "data": [{"msg": "ok", "processName": "p", "processStatus": "s"}]},
            {"msg_type": -2, "data": {"id": "n1", "title": "Done", "content": "C"}},
        ]
        response = self._make_response(events)
        cb = Mock(spec=StreamCallback)
        note = handle_stream_response(response, callback=cb)
        assert note.id == "n1"
        cb.on_init.assert_called_once_with("n1")
        cb.on_progress.assert_called_once()
        cb.on_complete.assert_called_once()

    def test_no_callback(self):
        events = [
            {"msg_type": -1, "data": {"note_id": "n1"}},
            {"msg_type": -2, "data": {"id": "n1", "title": "T", "content": "C"}},
        ]
        response = self._make_response(events)
        note = handle_stream_response(response)
        assert note.id == "n1"

    def test_no_note_raises(self):
        events = [{"msg_type": -1, "data": {"note_id": "n1"}}]
        response = self._make_response(events)
        with pytest.raises(GetBijiError, match="without receiving"):
            handle_stream_response(response)

    def test_audio_chapter_event(self):
        events = [
            {"msg_type": -6, "data": {"chapter": "info"}},
            {"msg_type": -2, "data": {"id": "n1", "title": "T", "content": "C"}},
        ]
        response = self._make_response(events)
        cb = Mock(spec=StreamCallback)
        note = handle_stream_response(response, callback=cb)
        assert note.id == "n1"

    def test_init_without_note_id(self):
        events = [
            {"msg_type": -1, "data": {}},
            {"msg_type": -2, "data": {"id": "n1", "title": "T", "content": "C"}},
        ]
        response = self._make_response(events)
        cb = Mock(spec=StreamCallback)
        note = handle_stream_response(response, callback=cb)
        assert note.id == "n1"
        cb.on_init.assert_not_called()


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