"""Unit tests for source connector registry and interfaces."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from extractor.sources.base import SourceFile
from extractor.sources.registry import get_source, list_schemes


def test_list_schemes_includes_http():
    # http/https are registered by importing extractor.sources.http (via register.py)
    import extractor.sources.http  # noqa: F401 — triggers registration

    schemes = list_schemes()
    assert "http" in schemes
    assert "https" in schemes


def test_get_source_unknown_raises():
    with pytest.raises(ImportError):
        get_source("unknown_scheme_xyz_abc")


def test_source_file_dataclass_defaults():
    sf = SourceFile(
        uri="s3://my-bucket/doc.pdf",
        filename="doc.pdf",
        size_bytes=1024,
        etag="abc123",
    )
    assert sf.uri == "s3://my-bucket/doc.pdf"
    assert sf.filename == "doc.pdf"
    assert sf.etag == "abc123"
    assert sf.size_bytes == 1024
    assert sf.version_id is None
    assert sf.last_modified is None
    assert sf.content_type is None
    assert sf.extra == {}


def test_source_file_all_fields():
    sf = SourceFile(
        uri="s3://bucket/key.pdf",
        filename="key.pdf",
        size_bytes=2048,
        etag="etag_val",
        version_id="v1",
        last_modified="2024-01-01T00:00:00Z",
        content_type="application/pdf",
        extra={"custom": "value"},
    )
    assert sf.version_id == "v1"
    assert sf.last_modified == "2024-01-01T00:00:00Z"
    assert sf.content_type == "application/pdf"
    assert sf.extra == {"custom": "value"}


def test_http_source_list_mock():
    """HttpSource.list() yields one SourceFile from a HEAD response."""
    from extractor.sources.http import HttpSource

    source = HttpSource()

    mock_resp = MagicMock()
    mock_resp.headers = {
        "Content-Length": "5000",
        "ETag": '"abc"',
        "Content-Type": "application/pdf",
    }
    mock_resp.raise_for_status = MagicMock()

    with patch("requests.head", return_value=mock_resp):
        files = list(source.list("https://example.com/doc.pdf"))

    assert len(files) == 1
    assert files[0].filename == "doc.pdf"
    assert files[0].size_bytes == 5000
    assert files[0].etag == "abc"  # stripped of quotes


def test_http_source_list_no_content_length():
    """HttpSource.list() handles missing Content-Length gracefully."""
    from extractor.sources.http import HttpSource

    source = HttpSource()

    mock_resp = MagicMock()
    mock_resp.headers = {"Content-Type": "application/pdf"}
    mock_resp.raise_for_status = MagicMock()

    with patch("requests.head", return_value=mock_resp):
        files = list(source.list("https://example.com/report.pdf"))

    assert len(files) == 1
    assert files[0].size_bytes is None  # 0 maps to None


def test_http_source_fetch_downloads_to_tempfile():
    """HttpSource.fetch() downloads content to a temp file."""
    from extractor.sources.http import HttpSource

    source = HttpSource()
    sf = SourceFile(uri="https://example.com/doc.pdf", filename="doc.pdf")

    mock_resp = MagicMock()
    mock_resp.__enter__ = lambda s: s
    mock_resp.__exit__ = MagicMock(return_value=False)
    mock_resp.raise_for_status = MagicMock()
    mock_resp.iter_content = MagicMock(return_value=[b"fake pdf content"])

    captured_path = None
    with patch("requests.get", return_value=mock_resp), source.fetch(sf) as tmp_path:
        captured_path = tmp_path
        assert tmp_path.exists()
        assert tmp_path.suffix == ".pdf"
        content = tmp_path.read_bytes()
        assert content == b"fake pdf content"
    # File cleaned up after context exit
    assert not captured_path.exists()


def test_http_source_fetch_size_limit():
    """HttpSource.fetch() raises ValueError if download exceeds max bytes."""

    from extractor.sources.http import _MAX_BYTES_DEFAULT, HttpSource

    source = HttpSource()
    sf = SourceFile(uri="https://example.com/big.pdf", filename="big.pdf")

    big_chunk = b"x" * 1024
    n_chunks = (_MAX_BYTES_DEFAULT // 1024) + 2

    mock_resp = MagicMock()
    mock_resp.__enter__ = lambda s: s
    mock_resp.__exit__ = MagicMock(return_value=False)
    mock_resp.raise_for_status = MagicMock()
    mock_resp.iter_content = MagicMock(return_value=[big_chunk] * n_chunks)

    with patch("requests.get", return_value=mock_resp), pytest.raises(ValueError, match="exceeded"):  # noqa: SIM117
        with source.fetch(sf) as _:
            pass


def test_http_source_fetch_custom_size_limit(monkeypatch):
    """HttpSource.fetch() respects EXTRACTOR_HTTP_MAX_BYTES env var."""
    from extractor.sources.http import HttpSource

    monkeypatch.setenv("EXTRACTOR_HTTP_MAX_BYTES", "10")
    source = HttpSource()
    sf = SourceFile(uri="https://example.com/big.pdf", filename="big.pdf")

    mock_resp = MagicMock()
    mock_resp.__enter__ = lambda s: s
    mock_resp.__exit__ = MagicMock(return_value=False)
    mock_resp.raise_for_status = MagicMock()
    mock_resp.iter_content = MagicMock(return_value=[b"x" * 100])

    with patch("requests.get", return_value=mock_resp), pytest.raises(ValueError, match="exceeded"):  # noqa: SIM117
        with source.fetch(sf) as _:
            pass


def test_https_source_registered():
    """HttpsSource is registered under the 'https' scheme."""
    import extractor.sources.http  # noqa: F401
    from extractor.sources.registry import get_source

    src = get_source("https")
    assert src.scheme == "https"


def test_s3_source_registered():
    """S3Source registers under 's3' scheme."""
    import extractor.sources.s3  # noqa: F401
    from extractor.sources.registry import get_source

    src = get_source("s3")
    assert src.scheme == "s3"


def test_s3_source_requires_boto3(monkeypatch):
    """S3Source._get_client raises ImportError if boto3 not installed."""
    from extractor.sources.s3 import S3Source

    src = S3Source()
    # Patch boto3 import to fail inside _get_client
    import builtins

    real_import = builtins.__import__

    def mock_import(name, *args, **kwargs):
        if name == "boto3":
            raise ImportError("No module named 'boto3'")
        return real_import(name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", mock_import)
    # Force client to None so it tries to import
    src._client = None
    with pytest.raises(ImportError, match="boto3"):
        src._get_client()


def test_minio_source_registered():
    """MinIOSource registers under 'minio' scheme."""
    import extractor.sources.s3  # noqa: F401
    from extractor.sources.registry import get_source

    src = get_source("minio")
    assert src.scheme == "minio"


def test_register_source_custom():
    """Custom source connectors can be registered by instance."""
    from extractor.sources.registry import get_source, register_source

    class FtpSource:
        scheme = "ftp_test_unique"

        def list(self, uri, glob_pattern=None):
            return iter([])

        def fetch(self, source_file, tmp_dir=None):
            pass

    instance = FtpSource()
    register_source(instance)
    result = get_source("ftp_test_unique")
    assert result is instance
