"""Unit tests for sink registry and sink interfaces."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from extractor.sinks import get_sink, list_sinks, register_sink
from extractor.sinks.base import BaseSink


def test_list_sinks_includes_builtins():
    # Import triggers registration via __init__.py
    sinks = list_sinks()
    for name in [
        "clickhouse",
        "mongodb",
        "postgresql",
        "elasticsearch",
        "qdrant",
        "weaviate",
        "kafka",
        "webhook",
    ]:
        assert name in sinks, f"{name!r} not found in {sinks}"


def test_get_sink_unknown_raises():
    with pytest.raises(ImportError, match="not_a_real_sink"):
        get_sink("not_a_real_sink")


def test_register_and_get_custom_sink():
    class DummySink(BaseSink):
        def connect(self):
            pass

        def write_batch(self, records):
            pass

        def flush(self):
            pass

        def close(self):
            pass

    register_sink("test_dummy_unique_xyz", DummySink)
    sink = get_sink("test_dummy_unique_xyz")
    assert isinstance(sink, DummySink)


def test_webhook_sink_no_extra_needed():
    """WebhookSink requires only requests which is a core dep."""
    from extractor.sinks.webhook import WebhookSink

    sink = WebhookSink(url="http://localhost:9999")
    assert sink.url == "http://localhost:9999"


@patch("extractor.sinks.webhook._check_ssrf")
def test_webhook_sink_feed_buffers(_mock_ssrf):  # noqa: ANN001
    from extractor.sinks.webhook import WebhookSink

    sink = WebhookSink(url="http://localhost:9999", batch_size=3)
    sink.connect()
    with patch("requests.post") as mock_post:
        sink.feed({"id": "a", "text": "hello"})
        sink.feed({"id": "b", "text": "world"})
        mock_post.assert_not_called()
        # Third record triggers batch flush
        sink.feed({"id": "c", "text": "!"})
        mock_post.assert_called_once()


@patch("extractor.sinks.webhook._check_ssrf")
def test_webhook_sink_flush(_mock_ssrf):  # noqa: ANN001
    from extractor.sinks.webhook import WebhookSink

    sink = WebhookSink(url="http://localhost:9999", batch_size=100)
    sink.connect()
    sink.feed({"id": "a", "text": "hello"})
    with patch("requests.post") as mock_post:
        mock_resp = MagicMock()
        mock_resp.raise_for_status = MagicMock()
        mock_post.return_value = mock_resp
        sink.flush()
        mock_post.assert_called_once()


@patch("extractor.sinks.webhook._check_ssrf")
def test_webhook_sink_total_inserted(_mock_ssrf):  # noqa: ANN001
    from extractor.sinks.webhook import WebhookSink

    sink = WebhookSink(url="http://localhost:9999", batch_size=2)
    sink.connect()
    mock_resp = MagicMock()
    mock_resp.raise_for_status = MagicMock()
    with patch("requests.post", return_value=mock_resp):
        sink.feed({"id": "a"})
        sink.feed({"id": "b"})  # triggers batch of 2
        assert sink.total_inserted == 2


def test_webhook_sink_requires_url():
    from extractor.sinks.webhook import WebhookSink

    with pytest.raises(ValueError, match="URL"):
        WebhookSink(url="")


def test_mongodb_sink_class_exists():
    """MongoDBSink class is importable."""
    from extractor.sinks.mongodb import MongoDBSink

    assert MongoDBSink is not None


def test_mongodb_sink_raises_without_pymongo(monkeypatch):
    """MongoDBSink raises ImportError when pymongo not available."""
    import extractor.sinks.mongodb as m

    original = m.pymongo
    m.pymongo = None
    try:
        with pytest.raises(ImportError, match="mongodb"):
            m.MongoDBSink()
    finally:
        m.pymongo = original


def test_base_sink_context_manager():
    class MinimalSink(BaseSink):
        def __init__(self):
            self.connected = False
            self.flushed = False
            self.closed = False

        def connect(self):
            self.connected = True

        def write_batch(self, records):
            pass

        def flush(self):
            self.flushed = True

        def close(self):
            self.closed = True

    sink = MinimalSink()
    with sink:
        assert sink.connected
    # __exit__ calls flush then close
    assert sink.flushed
    assert sink.closed


def test_base_sink_write_batch_abstract():
    """BaseSink cannot be instantiated directly."""
    with pytest.raises(TypeError):
        BaseSink()  # type: ignore[abstract]


@patch("extractor.sinks.webhook._check_ssrf")
def test_webhook_write_batch_posts_json(
    _mock_ssrf,
):
    from extractor.sinks.webhook import WebhookSink

    sink = WebhookSink(url="http://localhost:9999")
    mock_resp = MagicMock()
    mock_resp.raise_for_status = MagicMock()
    with patch("requests.post", return_value=mock_resp) as mock_post:
        sink.write_batch([{"id": "x", "text": "hello"}])
        mock_post.assert_called_once()
        call_kwargs = mock_post.call_args
        assert call_kwargs[0][0] == "http://localhost:9999"


def test_webhook_write_batch_empty_is_noop():
    from extractor.sinks.webhook import WebhookSink

    sink = WebhookSink(url="http://localhost:9999")
    with patch("requests.post") as mock_post:
        sink.write_batch([])
        mock_post.assert_not_called()
