"""Tests for streaming result export."""

from __future__ import annotations

import io
import json

from orxaq.fabric.streaming import (
    ResultStream,
    StreamProgress,
    chunked_processor,
    paginate,
    stream_ndjson,
    stream_sse,
)


def _sample_records(n: int = 5) -> list[dict]:
    return [{"id": i, "value": i * 10} for i in range(n)]


# ── StreamProgress ──────────────────────────────────────


class TestStreamProgress:
    def test_defaults(self):
        p = StreamProgress()
        assert p.total == 0
        assert p.processed == 0
        assert p.errors == 0

    def test_percent_zero_total(self):
        p = StreamProgress(total=0, processed=5)
        assert p.percent == 0.0

    def test_percent_normal(self):
        p = StreamProgress(total=100, processed=50)
        assert p.percent == 50.0

    def test_percent_over(self):
        p = StreamProgress(total=10, processed=20)
        assert p.percent == 100.0

    def test_to_dict(self):
        p = StreamProgress(total=10, processed=5)
        d = p.to_dict()
        assert d["total"] == 10
        assert d["processed"] == 5
        assert d["percent"] == 50.0
        assert "elapsed_ms" in d
        assert "rate_per_sec" in d

    def test_rate(self):
        p = StreamProgress(total=100, processed=50)
        assert p.rate >= 0


# ── stream_ndjson ───────────────────────────────────────


class TestStreamNDJSON:
    def test_basic(self):
        records = _sample_records(3)
        lines = stream_ndjson(records)
        assert len(lines) == 3
        parsed = json.loads(lines[0])
        assert parsed["id"] == 0

    def test_with_output(self):
        buf = io.StringIO()
        records = _sample_records(2)
        stream_ndjson(records, output=buf)
        buf.seek(0)
        written = buf.read()
        assert written.count("\n") == 2

    def test_progress_callback(self):
        progress_log = []
        records = _sample_records(3)
        stream_ndjson(
            records,
            on_progress=lambda p: progress_log.append(p.processed),
            total=3,
        )
        assert progress_log == [1, 2, 3]

    def test_empty(self):
        lines = stream_ndjson([])
        assert lines == []

    def test_iterator_input(self):
        def gen():
            for i in range(3):
                yield {"i": i}

        lines = stream_ndjson(gen())
        assert len(lines) == 3


# ── stream_sse ──────────────────────────────────────────


class TestStreamSSE:
    def test_basic(self):
        records = _sample_records(2)
        events = stream_sse(records)
        # 2 data events + 1 done event
        assert len(events) == 3
        assert events[0].startswith("event: result\n")
        assert events[-1].startswith("event: done\n")

    def test_custom_event_type(self):
        events = stream_sse([{"a": 1}], event_type="ecl")
        assert "event: ecl\n" in events[0]

    def test_with_output(self):
        buf = io.StringIO()
        stream_sse([{"x": 1}], output=buf)
        buf.seek(0)
        content = buf.read()
        assert "event: result" in content
        assert "event: done" in content

    def test_done_has_progress(self):
        events = stream_sse(_sample_records(3), total=3)
        done_data = events[-1].split("data: ")[1].strip()
        parsed = json.loads(done_data)
        assert parsed["processed"] == 3
        assert parsed["total"] == 3

    def test_progress_callback(self):
        counts = []
        stream_sse(
            _sample_records(3),
            on_progress=lambda p: counts.append(p.processed),
            total=3,
        )
        assert counts == [1, 2, 3]


# ── paginate ────────────────────────────────────────────


class TestPaginate:
    def test_first_page(self):
        records = _sample_records(10)
        result = paginate(records, page=1, page_size=3)
        assert result["page"] == 1
        assert result["total_records"] == 10
        assert result["total_pages"] == 4
        assert len(result["records"]) == 3
        assert result["has_next"] is True
        assert result["has_prev"] is False

    def test_last_page(self):
        records = _sample_records(10)
        result = paginate(records, page=4, page_size=3)
        assert len(result["records"]) == 1
        assert result["has_next"] is False
        assert result["has_prev"] is True

    def test_single_page(self):
        records = _sample_records(3)
        result = paginate(records, page=1, page_size=10)
        assert result["total_pages"] == 1
        assert result["has_next"] is False
        assert result["has_prev"] is False

    def test_empty_records(self):
        result = paginate([], page=1, page_size=10)
        assert result["total_records"] == 0
        assert result["total_pages"] == 1
        assert result["records"] == []

    def test_page_out_of_range(self):
        records = _sample_records(5)
        result = paginate(records, page=100, page_size=5)
        assert result["page"] == 1  # clamped to max page

    def test_invalid_page_zero(self):
        records = _sample_records(5)
        result = paginate(records, page=0, page_size=5)
        assert result["page"] == 1

    def test_invalid_page_size(self):
        records = _sample_records(5)
        result = paginate(records, page=1, page_size=-1)
        assert result["page_size"] == 50


# ── chunked_processor ───────────────────────────────────


class TestChunkedProcessor:
    def test_basic(self):
        records = _sample_records(5)
        results = chunked_processor(
            records,
            handler=lambda r: {"doubled": r["value"] * 2},
        )
        assert len(results) == 5
        assert results[0]["doubled"] == 0
        assert results[4]["doubled"] == 80

    def test_skip_none(self):
        records = _sample_records(5)
        results = chunked_processor(
            records,
            handler=lambda r: r if r["id"] % 2 == 0 else None,
        )
        assert len(results) == 3  # ids 0, 2, 4

    def test_progress_callback(self):
        progress_log = []
        chunked_processor(
            _sample_records(3),
            handler=lambda r: r,
            on_progress=lambda p: progress_log.append(p.processed),
        )
        assert progress_log == [1, 2, 3]

    def test_on_chunk(self):
        chunks: list[list[dict]] = []
        chunked_processor(
            _sample_records(5),
            handler=lambda r: r,
            chunk_size=2,
            on_chunk=lambda c: chunks.append(list(c)),
        )
        assert len(chunks) == 3  # 2 + 2 + 1

    def test_error_handling(self):
        def bad_handler(r):
            if r["id"] == 2:
                msg = "bad"
                raise ValueError(msg)
            return r

        progress_log = []
        results = chunked_processor(
            _sample_records(5),
            handler=bad_handler,
            on_progress=lambda p: progress_log.append((p.processed, p.errors)),
        )
        assert len(results) == 4  # 5 - 1 error
        # Check error count at position where id=2 was processed
        assert any(errors > 0 for _, errors in progress_log)


# ── ResultStream ────────────────────────────────────────


class TestResultStream:
    def test_iter_ndjson(self):
        stream = ResultStream(_sample_records(3), total=3)
        lines = list(stream.iter_ndjson())
        assert len(lines) == 3
        assert stream.progress.processed == 3

    def test_iter_sse(self):
        stream = ResultStream(_sample_records(2), total=2)
        events = list(stream.iter_sse())
        assert len(events) == 3  # 2 + done
        assert events[-1].startswith("event: done")

    def test_collect(self):
        stream = ResultStream(_sample_records(3))
        records = stream.collect()
        assert len(records) == 3

    def test_collect_from_generator(self):
        def gen():
            for i in range(3):
                yield {"i": i}

        stream = ResultStream(gen())
        records = stream.collect()
        assert len(records) == 3

    def test_to_json(self):
        stream = ResultStream(_sample_records(2))
        j = stream.to_json()
        parsed = json.loads(j)
        assert len(parsed) == 2

    def test_progress_callback(self):
        progress_log = []
        stream = ResultStream(
            _sample_records(3),
            total=3,
            on_progress=lambda p: progress_log.append(p.processed),
        )
        list(stream.iter_ndjson())
        assert progress_log == [1, 2, 3]

    def test_sse_progress_callback(self):
        """Cover line 236: iter_sse with on_progress callback."""
        progress_log = []
        stream = ResultStream(
            _sample_records(3),
            total=3,
            on_progress=lambda p: progress_log.append(p.processed),
        )
        events = list(stream.iter_sse())
        # 3 result events + 1 done event
        assert len(events) == 4
        assert progress_log == [1, 2, 3]
        assert "event: done" in events[-1]


# ── Targeted coverage (line 41) ──────────────────────────


class TestStreamProgressZeroElapsed:
    """Cover line 41: rate returns 0.0 when elapsed <= 0."""

    def test_rate_zero_when_future_start(self):
        import time

        p = StreamProgress(total=10, processed=5, started_at=time.time() + 1000)
        assert p.rate == 0.0
